diff --git a/.assets-revision b/.assets-revision index fabc7ecf..db39ae3b 100644 --- a/.assets-revision +++ b/.assets-revision @@ -5,4 +5,4 @@ # is a git revision (branch name like `main`, a tag, or a specific commit # sha). Override at runtime with the ASSETS_REVISION env var. repo: ChilleD/WebHarbor -revision: 65c479f894763f64c6073e0d180ebf542d1d2c02 +revision: f9ddfd2596229f2610418d57fc88c3051e1056bb diff --git a/AGENTS.md b/AGENTS.md index 22696c27..bed4ce67 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ A coding agent (Claude Code, Cursor, Aider, Codex, ...) is reading this. Read on ## What it is -23 Flask mirror websites (Amazon, GitHub, BBC News, ...) packaged into one Docker image, plus a control plane on `:8101` for resetting per-site state. Used as a deterministic offline environment for web-agent benchmarks. ~3 GB image. +25 Flask mirror websites (Amazon, GitHub, BBC News, ...) packaged into one Docker image, plus a control plane on `:8101` for resetting per-site state. Used as a deterministic offline environment for web-agent benchmarks. ~3 GB image. Two repos: - **code** (this one) — Flask apps, control plane, scripts. @@ -48,17 +48,17 @@ Inside the image, sites live at `/opt/WebSyn//`. The path predates the ren # fresh clone ./scripts/fetch_assets.sh # pulls assets from HF ./scripts/build.sh # docker build -t webharbor:dev . -docker run -d -p 8101:8101 -p 40000-40023:40000-40023 webharbor:dev +docker run -d -p 8101:8101 -p 40000-40024:40000-40024 webharbor:dev ``` -Or use the published image directly: +For the separately released published image (which may lag this source branch): ```bash -docker run -d -p 8101:8101 -p 40000-40023:40000-40023 \ +docker run -d -p 8101:8101 -p 40000-40024:40000-40024 \ battalion7244/webharbor:latest ``` -Sites are on `40000`-`40023` in the order declared by `SITES=( ... )` in `websyn_start.sh`. Control plane: +Sites are on `40000`-`40024` in the order declared by `SITES=( ... )` in `websyn_start.sh`. Control plane: | Method | Path | Purpose | |--------|---------------------|-------------------------------------------| @@ -136,13 +136,13 @@ python3 -m py_compile sites//app.py # 3. run on alt ports (don't collide with anything you already have running) docker run -d --rm --name wh-test \ - -p 8201:8101 -p 41000-41023:40000-40023 webharbor:dev + -p 8201:8101 -p 41000-41024:40000-40024 webharbor:dev # 4. control plane healthy, all sites alive curl -s http://localhost:8201/health | python3 -m json.tool | head # 5. every site renders 200 -for p in $(seq 41000 41023); do +for p in $(seq 41000 41024); do curl -so /dev/null -w "$p:%{http_code}\n" http://localhost:$p/ done diff --git a/CLAUDE.md b/CLAUDE.md index 12e3cb0f..ad0e2fd0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,4 +16,4 @@ The full agent guide is loaded above via `@AGENTS.md`. The notes below apply onl ## Existing containers -If a container is already running on `:8101` / `:40000-40023`, treat it as the user's working environment — don't `docker stop` or `docker rm` it without explicit confirmation. Spin up your test container under a different name on alt ports (`:8201`, `:41000-41023`). +If a container is already running on `:8101` / `:40000-40024`, treat it as the user's working environment — don't `docker stop` or `docker rm` it without explicit confirmation. Spin up your test container under a different name on alt ports (`:8201`, `:41000-41024`). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f36384c..1029fa9b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ git clone https://github.com//webharbor && cd webharbor ./scripts/fetch_assets.sh # pull current assets ./scripts/new_site.py mywebsite # OR edit an existing site ./scripts/build.sh && docker run -d --rm \ - -p 8101:8101 -p 40000-40023:40000-40023 webharbor:dev + -p 8101:8101 -p 40000-40024:40000-40024 webharbor:dev # iterate locally... ./scripts/extract_assets.sh ../webharbor-static-pr/ # split assets out diff --git a/Dockerfile b/Dockerfile index 86c17615..deab9600 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # WebHarbor — slim, self-contained image. -# 24 Flask mirror sites + control plane on :8101. +# 25 Flask mirror sites + control plane on :8101. FROM python:3.12-slim-bookworm @@ -72,6 +72,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-40023 +EXPOSE 8101 40000-40024 CMD ["/opt/websyn_start.sh"] diff --git a/README.md b/README.md index a05d266a..3388d447 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,15 @@ git clone https://github.com/aiming-lab/WebHarbor && cd WebHarbor ./scripts/build.sh # docker build -t webharbor:dev . ``` +This source branch also includes IMDb at port `40024` (25 mirrors total). After fetching the pinned assets and building locally, run it with: + +```bash +docker run -p 8101:8101 -p 40000-40024:40000-40024 webharbor:dev +``` + +The published image above is released separately; use the local build to review the IMDb contribution. + + ## 🤝 Contribute We have built 23 high-quality mirrors covering the [WebVoyager](https://github.com/MinorJerry/WebVoyager) benchmark. The next goal is **100+ sites**, covering everything in [Online-Mind2Web](https://huggingface.co/datasets/osunlp/Online-Mind2Web). We are inviting the community to build this together. diff --git a/control_server.py b/control_server.py index 7df0d9ee..281c51d9 100644 --- a/control_server.py +++ b/control_server.py @@ -26,7 +26,8 @@ 'allrecipes', 'amazon', 'apple', 'arxiv', 'bbc_news', 'booking', 'github', 'google_flights', 'google_map', 'google_search', 'huggingface', 'wolfram_alpha', 'cambridge_dictionary', - 'coursera', 'espn', 'merriam_webster', 'ikea', 'phys_org', 'target', 'ted', 'osu', 'rotten_tomatoes', 'compass', 'walmart_careers', + 'coursera', 'espn', 'merriam_webster', 'ikea', 'phys_org', 'target', 'ted', 'osu', + 'rotten_tomatoes', 'compass', 'walmart_careers', 'imdb', ] BASE_PORT = 40000 WEBSYN_DIR = '/opt/WebSyn' diff --git a/review-reports/PR-33-IMDB.md b/review-reports/PR-33-IMDB.md new file mode 100644 index 00000000..9235a939 --- /dev/null +++ b/review-reports/PR-33-IMDB.md @@ -0,0 +1,375 @@ +# IMDb reviewer validation + +Status: **ready for maintainer review; nothing is merged.** The reviewed candidate is +the fixed pair below. Twenty accepted tasks each have a complete sealed UI run, +deterministic PASS, and an independent frozen-run PASS from a formal 20-task blind +review of this candidate; the Owner accepted the expanded visual result on 2026-09-12 +after comparing it with live IMDb. This report revision is a docs-only commit on top +of the implementation fixed point; only files under `review-reports/` change. + +This review preserves [@hqhq1025's original IMDb contribution, PR #33](https://github.com/aiming-lab/WebHarbor/pull/33) +and its commit history. The reviewer branch integrates `main` at +`36004932bdf82afbe36dc14e00f66841eccf9946`, including OSU, Rotten Tomatoes, Compass and +Walmart Careers, and appends IMDb at **40024** (25 registered sites). + +## Fixed candidate + +| Item | Value | +|---|---| +| Implementation fixed point (code) | `1c8a1eba19c084e202ecbab5977280701b9068c2` | +| Base | `36004932bdf82afbe36dc14e00f66841eccf9946` (`main`) | +| HF asset PR | [ChilleD/WebHarbor #57](https://huggingface.co/datasets/ChilleD/WebHarbor/discussions/57), open, no conflicting files reported | +| Pinned HF revision (`.assets-revision`) | `f9ddfd2596229f2610418d57fc88c3051e1056bb` (`refs/pr/57`) | +| `imdb.tar.gz` | 39,111,545 bytes, SHA256 `0663d30fe90ed2a0659dfe78ecf7ab1718970f565a64c6855a2c6aaadc606513` | +| Seed `instance_seed/imdb.db` | SHA256 `9d843c5388ecbc0d5265ac316cdaee915f8b203121b5a5330d207af5fb6206b0` | +| `sites/imdb/tasks.jsonl` | SHA256 `36214b6cf168c726d60e1ea0662ddfbfce6fa5dc77c804812460d1c38da545d8`, 20 rows | +| Local immutable check image | `sha256:edf9bbbd019813665fc169e39174db70c11969dcf182260f04d8f81636802dad` | + +HF PR #57 lineage: `4d5709e` (first reviewer seed) → `e70f49d` (title-year +corrections) → `c2791ad` (union with asset main `18e64e4d`) → `f9ddfd25` (homepage and +editorial media plus the updated seed). Only `imdb.tar.gz` changes at `f9ddfd25` +relative to `c2791ad`; every other dataset path is byte-identical. The archive was +independently re-downloaded through the official HF client and matched the local +candidate on archive, seed and all 4,536 members (247 members added, none removed, +all pre-existing image/cache hashes unchanged). Asset main has since advanced to +`ad6f424f` with two unrelated archives (`fedex.tar.gz`, `webmd_doctor.tar.gz`); PR #57 +still adds only `imdb.tar.gz` and reports no conflicts. + +## Asset and source corrections + +The original loader accepted unrelated profile payloads under a requested person ID. +It now checks the canonical IMDb identity. The asset migration uses exact official +`nconst` records to correct 642 corroborated profile collisions and canonicalize 47 +uncertain alias/profile records, plus two independently supported birth years. It also +normalizes 390 complete release dates without inventing missing dates. Memento and +Schindler's List use their canonical title years while preserving their later regional +release dates. Credits, identifiers, synthetic users, reviews, ratings and watchlists +are preserved. + +For 689 affected profiles, unsupported biography, birthplace and portrait links are +cleared; replacement text or portraits have not been generated. This is a +source-coverage limitation and does not mean that every retained image file was +individually proven wrong. See the +[source provenance and reproducible migration](../sites/imdb/docs/seed-provenance.md). +The original [asset PR #23](https://huggingface.co/datasets/ChilleD/WebHarbor/discussions/23) +is linked to this candidate; maintainers should coordinate the two so that the old seed +is not reintroduced. + +IMDb's live ratings, box-office totals and front-page content change. This mirror +contains a fixed catalog; current source observations and historical values are +distinguished. The local **Domestic box office** page explicitly ranks catalog titles by +cumulative US & Canada gross and is not represented as IMDb's dated weekend chart. Test +users, watchlists, personal ratings and user reviews are synthetic benchmark state. + +## Homepage and editorial expansion + +The homepage now separates sourced editorial content from the benchmark catalog. A +separate `home_features` table holds 219 source rows captured from the logged-out IMDb +homepage on 2026-09-10 after lazy-loaded sections appeared: nine trailer artwork +previews, eight editorial previews, four topic links, one episode spotlight with eight +episode scores, five news previews, 25 streaming titles, 37 TV-schedule cards, 30 +birthdays and the captured top-100 STARmeter. The +[What to Watch in September](../sites/imdb/docs/most-anticipated-2026-09-12.json) +editorial route was observed separately on 2026-09-12 (17 entries) and has a local +detail route per entry. All media is served locally; the importers validate official +source URLs and local file hashes and are build-time tools, not boot or reset hooks. +Sources, capture times and hashes are recorded in +[`homepage-sources.json`](../sites/imdb/docs/homepage-sources.json), +[`starmeter-sources.json`](../sites/imdb/docs/starmeter-sources.json) and the +[homepage snapshot notes](../sites/imdb/docs/homepage-snapshot.md), which also list +the remaining fidelity gaps (no trailer video files, editorial destinations show the +sourced preview only, no episode pages, no live showtimes/ticketing/playback, offline +information pages for app/commercial/social destinations). + +Browser checks on the fixed candidate (Chromium, 1440×900 and 390×844): homepage and +`/feature/featured-today-1` returned HTTP 200 with no horizontal overflow; the feature +page rendered all 17 entries with locally bundled images; all 17 detail routes returned +200 with no overflow. Anonymous headless Chromium receives IMDb's 403, so the source +comparison relies on the Owner's browser session and the frozen source fields rather +than a new automated source capture. + +| View | Candidate (2026-09-12) | +|---|---| +| Home, 1440 wide, top of page | ![Candidate home](assets/pr33-imdb/round4-home-1440.jpg) | +| Feature route, 1440 wide, top of page | ![Candidate feature](assets/pr33-imdb/round4-feature-1440.jpg) | +| Feature detail, 1440×900 | ![Candidate feature detail](assets/pr33-imdb/round4-feature-detail-1440.jpg) | +| Home, 390 wide, top of page | ![Candidate home 390](assets/pr33-imdb/round4-home-390.jpg) | + +The earlier 2026-09-08 source/before/candidate comparison is retained below for the +pre-expansion state. [Capture purposes and hashes](assets/pr33-imdb/evidence.json) +identify every included image. + +## Task and scoring scope + +The candidate retains original IDs `0, 2, 7, 9, 10, 12, 14, 15, 16, 17` and retires +`1, 3, 4, 5, 6, 8, 11, 13`. Earlier retirements cover duplicate/shallow lookups with +answers exposed on cards, a count of static filter options, and helpfulness selection +where the seed has only one review. The final quality pass also removes tasks 3 and 4 +because the first matching title detail exposes every requested value, and task 8 +because its actor/character/birth-year combination has a stable common-knowledge +shortcut. All corresponding site features and original frozen executions remain intact. + +To meet the Owner's minimum of twenty without restoring those weak tasks, the candidate +adds IDs `18–27`. Six are read-only relations or calculations: constrained pair +optimization, role-bound filmography intersection, full-filmography aggregation, +cross-account set subtraction, duplicate-review grouping and calendar-interval +comparison. Four are stateful: multi-item Watchlist qualification, cross-account +recommendation transfer, new-account queue persistence and a review-derived +personal-rating reconciliation. No prompt contains its answer, and no dated +editorial-page task was added merely to reach 21. + +The revisions add needed detail lookups, specify movie/TV scope, define ties, +distinguish monetary fields, and replace the two requests for an external human's +choice with deterministic selection from each synthetic user's initial watchlist. +Questions, English rubrics and verifier entry points are kept in one-to-one +correspondence; expected answers are not placed in task rows. + +The verifiers derive expected entities and values from the before snapshot. Read-only +tasks compare every business table. Stateful tasks permit only the specified row change +and require local UI action and subsequent page evidence. Alternative entry routes and +equivalent displayed monetary units are supported. Exact model prose is not required. + +The [verifier interface](../sites/imdb/verify/README.md) runs offline with the standard +library. Synthetic tests cover ambiguous ties, entity/field binding, failed or foreign +navigation, wrong account/target, no-op changes, additional writes and legitimate +alternatives. The repository's native `eval_judge.py --verifier True` has also been +exercised with a synthetic positive and a foreign-origin negative; both produced the +expected verdicts. These fixtures are not browser trajectories. Safe recorder +placeholders `[SUPPLIED PASSWORD]` and `[REDACTED]` are accepted only when the snapshot +password hash matches the task-supplied credential and the same-origin login/register +transition succeeds. + +## UI and functional review + +The navigation uses an accessible menu drawer and usable search; home cards use the +dark palette, charts show poster/rank rows, and title details use a dark overview with a +separate facts section. Desktop and 390px layouts were checked in a real browser on +the fixed candidate with no broken images or page-width overflow. The Owner accepted +the expanded result on 2026-09-12; the fidelity gaps listed in the homepage snapshot +notes are explicit scope decisions rather than open defects. + +| View | Original site | Before repair | Candidate (2026-09-08, pre-expansion) | +|---|---|---|---| +| Home, 1440×900 | ![IMDb home](assets/pr33-imdb/source-home.jpg) | ![Original mirror](assets/pr33-imdb/before-home.jpg) | ![Candidate mirror](assets/pr33-imdb/candidate-home.jpg) | +| Dark Knight, 1280×720 | ![IMDb detail](assets/pr33-imdb/source-detail.jpg) | Original audit retained separately | ![Candidate detail](assets/pr33-imdb/candidate-detail.jpg) | + +Source/before used IAB and candidate captures used Chrome; these are visual comparisons, +not a calibrated pixel regression baseline. Source advertising and editorial content are +dynamic. + +All state-changing forms use Flask-WTF CSRF protection, including both logout entry +points. Same-origin watchlist return paths are constrained, overflowing year input +returns 400, malformed email registration is rejected, and invalid review scores cannot +write a review. The application keeps its existing Flask/Jinja stack. + +A frozen guided browser audit covered registration/login, failed inputs, watchlist +add/remove, personal rating and written-review persistence. A real form hosted on a +different localhost port submitted a rating without a CSRF token: the application +returned 400 and the entire database remained unchanged +([rejection screenshot](assets/pr33-imdb/csrf-rejected.png)). The audit exposed one +account-page GET logout link missed by handler tests; it was converted to a protected +POST form and rechecked in a real browser +([post-fix confirmation](assets/pr33-imdb/account-logout.png)). Out-of-range review +ratings were tested at the handler boundary; the browser UI only offers valid scores. +The later homepage expansion did not change these handlers; every accepted task path was +replayed on the fixed candidate (see below). + +## Engineering results on the fixed candidate + +Checks inside the immutable image built from code `1c8a1eb` and HF `f9ddfd25`: + +| Check | Observed result | +|---|---| +| Full environment HTTP sweep | 25/25 HTTP 200 before and after the task regression | +| Control plane | 25/25 registered sites ready | +| IMDb dirty database reset | Runtime bytes equal the pinned seed SHA256 above | +| reset-all | All sites ready, 5.35 seconds | +| IMDb unit tests | 307 passed in 21.763 seconds | +| Guided replay of all 20 task paths from the homepage | 20/20 UI complete, 20/20 deterministic verifier PASS, 311 recorded steps, 13/13 read-only tasks left the database byte-identical, 7/7 write tasks produced the expected delta, 20/20 official resets restored the seed | + +The image carries matching code and asset-revision labels and has no host mounts. It +was assembled from the already verified shared dependency image plus the exact fixed +code and the downloaded HF archive because the host did not have the headroom used for +an unknown-peak full rebuild; it is not represented as a fresh standard-Dockerfile +build. The [reproduction](#reproduction) path below is the standard source build. The +guided replay is implementation-informed change-impact regression; it is not new +independent exploration, does not replace the sealed canonical runs and is not a blind +review. Per-task results are in +[`candidate-regression-1c8a1eb-summary.json`](assets/pr33-imdb/candidate-regression-1c8a1eb-summary.json). + +Historical results for code `50bcce5` (22-site integration at that time) are retained: +22/22 HTTP 200, byte-identical IMDb reset, reset-all 3.241 s, 146 and later 174 +engineering tests +([mechanical output](assets/pr33-imdb/mechanical-results.json), +[engineering tests](assets/pr33-imdb/engineering-tests.txt), +[final ten-task scorer tests](assets/pr33-imdb/engineering-tests-final.txt), +[scorer identities and results](assets/pr33-imdb/scoring-results.json)). + +## Frozen task executions and scoring + +Each of the 20 accepted tasks has one complete canonical UI run with actual actions, +step screenshots, DOM snapshots, a final answer and before/after database snapshots, +totaling 311 recorded steps. Executions were recorded on earlier reviewed versions and +are disclosed as such; none is relabeled as an execution at the fixed point: + +| Tasks | Code | HF revision | Seed SHA256 | +|---|---|---|---| +| 17 | r1 `ebe92f0` (commit plus recorded tracked patch) | `4d5709e` | `27558f13…` | +| 0, 2, 7, 9, 10, 12, 14, 15, 16 and 18–27 | r2 `50bcce5` | `e70f49d` | `69f849b9…` | + +Reuse basis for the fixed candidate: `tasks.jsonl` and the `sites/imdb/verify` tree are +byte-identical between the expansion freeze `3d9fc72` and `1c8a1eb`; the logical +contents of all ten business tables are identical between the r2 execution seed and the +current seed (the current seed only adds the `home_features` table); the r1 seed used by +task 17 differs only on two title-year corrections for titles that task never reads; +and the code changes after the freeze add the homepage, feature/collection/news/offline +routes and a cookie-session recently-viewed list without touching search, charts, +genre, name, watchlist, rating, review, login, register or logout handlers. The guided +replay above closes the navigation-reachability risk introduced by the new base template. + +The original ten runs contain 98 recorded steps and 80 successful substantive actions; +the ten expansion runs contain 219 recorded steps with no action or capture failure in +the selected runs. Task 18 preserves a one-step environment failure at an incorrect +viewport and task 21 an incomplete earlier attempt; neither is counted. Original run +files were never rewritten when scoring-format false negatives were fixed. Runners were +isolated from source, hidden answer keys and scoring outputs but retained their own +preceding UI context; runner models are self-declared in run metadata. These are not +fresh-context trials or an estimate of model success rate. +[Per-task results, paths and final screenshot excerpts](assets/pr33-imdb/tasks/task-table.md) +and the [structured record](assets/pr33-imdb/tasks/task-results.json) cover the original +ten; [public expansion counts](assets/pr33-imdb/task-expansion-summary.json) cover the +new ten. + +Deterministic grading: the final native regrade passes 10/10 retained original main +runs and 3/3 separately recorded guided alternatives (a different legal cutoff tie for +task 9, Watchlist row removal for 15, Most-recent review confirmation for 17); all 30 +applicable constructed cases (8 positive, 22 negative), seven historical controls and +one extra unit case match their prior expectations. The expansion runs pass 10/10 with +115/115 focused tests covering wrong owner, partial result, extra write, arbitrary +credential placeholder, incorrect grouping and field-binding negatives. The full IMDb +suite passes 307/307 on the fixed candidate. Fixtures are not browser runs and these +counts do not establish overall scorer accuracy. + +The real runs exposed false rejections of task 17's headline reference, task 0's rank +prefixes/runtime difference, task 12's group labels and task 16's correctly reported +previous rating, plus a missed nested Remove button locator. The repairs distinguish +those statements and controls without relaxing task requirements; wrong headline +suffixes, ranks, comparison differences, old/current ratings, entities and extra +database writes remain negative controls. + +Seven observed original paths used at least five successful substantive actions and +tasks 0, 12 and 14 perform comparisons; the expansion tasks add set, ratio and +multi-account reasoning over 11–35 steps each. Empirical frontier-model difficulty is +**NOT_VERIFIED**. + +## Independent blind review + +A formal blind review of the fixed candidate was completed on 2026-09-12 by Claude Code +with canonical model `claude-opus-5` (attested by the CLI usage record of the session, +which was fresh, non-persistent, restricted to read-only tools and had network tools +disabled). The input was a single neutral packet of 826 files covering all 20 canonical +runs (manifest SHA256 `09875b57a8c3f9092eae5f6f00fb619715dbecf5eeefd8f698ac2c5c5dc343fa`), +containing task text, answer-free rubric, trajectories, event/DOM evidence, screenshots, +final answers and before/after databases, and excluding verifier source/results, hidden +answers, coordinator conclusions, prior reviews, repository contents and PR discussion. +The reviewer verified 826/826 file hashes and returned **20 PASS / 0 FAIL**; the +structured result was hashed before it was read. Per-task bases and the reviewer's own +list of unexecuted checks are in +[`formal-blind-review-1c8a1eb-summary.json`](assets/pr33-imdb/formal-blind-review-1c8a1eb-summary.json); +the [PR comment](https://github.com/aiming-lab/WebHarbor/pull/89#issuecomment-5646790749) +records the same result. + +The reviewer explicitly did not certify source fidelity, deterministic verifier +correctness, runner independence or live replay, and spot-checked screenshots rather +than auditing every step image. Two earlier probe reviews (10/10 on 2026-09-08 for the +original ten and 10/10 on 2026-09-10 for the expansion ten; models session-declared) are +retained as history in +[`independent-review-summary.json`](assets/pr33-imdb/independent-review-summary.json) and +[`task-expansion-independent-review-summary.json`](assets/pr33-imdb/task-expansion-independent-review-summary.json). + +## Reconciliation + +Comparing the formal blind result with the deterministic results task by task found +**zero differences and zero substantive findings**; no task, rubric, verifier, seed or +application change resulted. Common-PASS spot checks of tasks 9, 15, 16 and 17 against +the frozen DOM and independently recomputed full-table database diffs confirmed the +exact single-row changes (one Watchlist deletion, one rating update, one review insert) +and unchanged state for the read-only run. Details are in +[`reconciliation-1c8a1eb-summary.json`](assets/pr33-imdb/reconciliation-1c8a1eb-summary.json). + +Two boundary rulings are retained: + +- **Task 19** is graded from the amounts the mirror displays, as its text requires: + `$30.1M / $52.0M` rounds to **57.9%**. Computing from hidden raw values would give + 57.8%; the unique maximum is unchanged. +- **Task 26** completed its registration, queue transfer, sign-out and re-login flow + with exactly one new user and two Watchlist rows, but its run-start and seal-time + metadata disagree (model label, prior-knowledge flag, runner identity wording, + human-intervention list, resumed-session note). The completion verdict stands on + direct run evidence; the run producer's provenance is not treated as a reliable + independent-exploration attestation. + +## Reproduction + +Fetch the pinned candidate and build the source branch: + +```bash +./scripts/fetch_assets.sh +./scripts/build.sh webharbor:review-imdb-pr33 +docker run -d --name wh-review033-candidate \ + -p 127.0.0.1:8961:8101 \ + -p 127.0.0.1:49000-49024:40000-40024 webharbor:review-imdb-pr33 +``` + +IMDb is then available at `http://localhost:49024/`. Run engineering tests with the +image's pinned Flask dependencies: + +```bash +docker exec wh-review033-candidate \ + python3 -m unittest discover -s /opt/WebSyn/imdb/tests -v +``` + +For a supplied run bundle, use an absolute run directory: + +```bash +uv run --project agent_demo python agent_demo/eval_judge.py \ + --verifier True --run_dir /absolute/path/to/run \ + --out /absolute/path/to/separate-scoring-result.json +``` + +## Known limitations + +- Canonical runs were recorded on r1/r2 code and assets; their reuse for the fixed + candidate rests on the identical task/verifier tree, logically identical business + tables and the guided change-impact replay, not on re-execution at `1c8a1eb`. +- Runner models are self-declared in run metadata; task 26's provenance metadata is + inconsistent (above). +- The blind review judged frozen execution completion only. +- The immutable check image was assembled from a verified dependency image rather than + rebuilt from the standard Dockerfile; the reproduction path above is the standard build. +- Empirical frontier-model difficulty is not measured. +- 689 profiles have cleared biography/birthplace/portrait fields awaiting sourced + replacements; the homepage snapshot notes list the remaining presentation gaps. +- The pinned HF revision is the open PR revision, not a main merge commit (see below). + +## Maintainer handoff + +**Directly mergeable now: no.** The code is mergeable against `main` (GitHub reports +MERGEABLE/CLEAN), but `.assets-revision` pins the open HF PR revision `f9ddfd25` +rather than a merged asset-main commit, and the project convention is to pin the HF +merge commit. Suggested order: + +1. Review and merge [HF PR #57](https://huggingface.co/datasets/ChilleD/WebHarbor/discussions/57). + It adds only `imdb.tar.gz` (LFS SHA256 `0663d30f…`) on top of asset main and reports + no conflicts; asset main currently contains two unrelated later archives. +2. Confirm the merged asset-main commit still serves `imdb.tar.gz` with SHA256 + `0663d30fe90ed2a0659dfe78ecf7ab1718970f565a64c6855a2c6aaadc606513` and seed SHA256 + `9d843c5388ecbc0d5265ac316cdaee915f8b203121b5a5330d207af5fb6206b0`. +3. Update `.assets-revision` on this branch to that merge commit (or confirm that + pinning the immutable PR revision is acceptable). Because the bytes are identical, + only `./scripts/fetch_assets.sh` plus the hash check above need to be repeated; no + task re-execution or new blind review is implied by a pin-only change. +4. Merge this PR, which supersedes #33, and close #33 and HF #23 with references so + the old seed is not reintroduced. + +The reviewer will not merge either the code PR or the HF PR. diff --git a/review-reports/assets/pr33-imdb/account-logout.png b/review-reports/assets/pr33-imdb/account-logout.png new file mode 100644 index 00000000..436e5f44 Binary files /dev/null and b/review-reports/assets/pr33-imdb/account-logout.png differ diff --git a/review-reports/assets/pr33-imdb/before-home.jpg b/review-reports/assets/pr33-imdb/before-home.jpg new file mode 100644 index 00000000..0d772625 Binary files /dev/null and b/review-reports/assets/pr33-imdb/before-home.jpg differ diff --git a/review-reports/assets/pr33-imdb/candidate-detail.jpg b/review-reports/assets/pr33-imdb/candidate-detail.jpg new file mode 100644 index 00000000..bef64393 Binary files /dev/null and b/review-reports/assets/pr33-imdb/candidate-detail.jpg differ diff --git a/review-reports/assets/pr33-imdb/candidate-home.jpg b/review-reports/assets/pr33-imdb/candidate-home.jpg new file mode 100644 index 00000000..894edcc4 Binary files /dev/null and b/review-reports/assets/pr33-imdb/candidate-home.jpg differ diff --git a/review-reports/assets/pr33-imdb/candidate-regression-1c8a1eb-summary.json b/review-reports/assets/pr33-imdb/candidate-regression-1c8a1eb-summary.json new file mode 100644 index 00000000..e2fd19f2 --- /dev/null +++ b/review-reports/assets/pr33-imdb/candidate-regression-1c8a1eb-summary.json @@ -0,0 +1,333 @@ +{ + "schema": "imdb-public-candidate-regression-summary/v1", + "created_at": "2026-09-12T03:33:22.081650Z", + "candidate": { + "code_sha": "1c8a1eba19c084e202ecbab5977280701b9068c2", + "working_tree_dirty": false, + "asset_revision": "f9ddfd2596229f2610418d57fc88c3051e1056bb", + "archive_sha256": "0663d30fe90ed2a0659dfe78ecf7ab1718970f565a64c6855a2c6aaadc606513", + "seed_sha256": "9d843c5388ecbc0d5265ac316cdaee915f8b203121b5a5330d207af5fb6206b0", + "task_file_sha256": "36214b6cf168c726d60e1ea0662ddfbfce6fa5dc77c804812460d1c38da545d8", + "image_id": "sha256:edf9bbbd019813665fc169e39174db70c11969dcf182260f04d8f81636802dad", + "image_basis": "Assembled from the previously verified shared dependency image plus the exact fixed code and the remotely downloaded HF archive; matching code/HF labels; no host mounts. Not a fresh standard-Dockerfile build (host lacked the 50 GiB headroom used for unknown-peak full rebuilds)." + }, + "purpose": "Impact-specific guided UI and deterministic-verifier revalidation on the immutable GitHub/HF round-4 candidate; historical independent runs remain immutable.", + "knowledge_limit": "Guided replay of prior successful paths by an implementation-informed operator; change-impact evidence only. Not independent exploration, not canonical trajectory credit, not a blind review.", + "mechanical": { + "sites_http_200": "25/25 before and after the task regression", + "control_plane_ready": "25/25", + "imdb_reset": "runtime SHA256 equals seed SHA256 after dirty reset", + "reset_all_seconds": 5.35, + "imdb_unittest": { + "passed": 307, + "failed": 0, + "seconds": 21.763 + } + }, + "guided_impact": { + "viewport": { + "width": 1440, + "height": 900 + }, + "tasks_expected": 20, + "tasks_completed": 20, + "ui_passed": 20, + "verifiers_passed": 20, + "recorded_steps": 311, + "read_only_tasks_unchanged": 13, + "write_tasks_with_delta": 7, + "resets_restored_seed": 20, + "tasks": [ + { + "task_id": "IMDb--0", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 5, + "read_only_database_unchanged": true, + "after_db_sha256": "9d843c5388ecbc0d5265ac316cdaee915f8b203121b5a5330d207af5fb6206b0", + "reset_restored_seed": true, + "final_url_path": "/title/tt0468569" + }, + { + "task_id": "IMDb--2", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 6, + "read_only_database_unchanged": true, + "after_db_sha256": "9d843c5388ecbc0d5265ac316cdaee915f8b203121b5a5330d207af5fb6206b0", + "reset_restored_seed": true, + "final_url_path": "/title/tt4154796#title-details" + }, + { + "task_id": "IMDb--7", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 5, + "read_only_database_unchanged": true, + "after_db_sha256": "9d843c5388ecbc0d5265ac316cdaee915f8b203121b5a5330d207af5fb6206b0", + "reset_restored_seed": true, + "final_url_path": "/name/nm0634240" + }, + { + "task_id": "IMDb--9", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 9, + "read_only_database_unchanged": true, + "after_db_sha256": "9d843c5388ecbc0d5265ac316cdaee915f8b203121b5a5330d207af5fb6206b0", + "reset_restored_seed": true, + "final_url_path": "/search/title?title_type=movie&year_from=&year_to=&rating_min=8.5&genre=drama&sort=rating" + }, + { + "task_id": "IMDb--10", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 15, + "read_only_database_unchanged": true, + "after_db_sha256": "9d843c5388ecbc0d5265ac316cdaee915f8b203121b5a5330d207af5fb6206b0", + "reset_restored_seed": true, + "final_url_path": "/title/tt0110912#title-details" + }, + { + "task_id": "IMDb--12", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 11, + "read_only_database_unchanged": true, + "after_db_sha256": "9d843c5388ecbc0d5265ac316cdaee915f8b203121b5a5330d207af5fb6206b0", + "reset_restored_seed": true, + "final_url_path": "/search/title?title_type=tvSeries&year_from=&year_to=&rating_min=&genre=crime&sort=rating" + }, + { + "task_id": "IMDb--14", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 7, + "read_only_database_unchanged": true, + "after_db_sha256": "9d843c5388ecbc0d5265ac316cdaee915f8b203121b5a5330d207af5fb6206b0", + "reset_restored_seed": true, + "final_url_path": "/title/tt1375666#title-details" + }, + { + "task_id": "IMDb--15", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 14, + "read_only_database_unchanged": false, + "after_db_sha256": "9b67a45ebb28a7cc7d745c371949d6a337de3b9e739b9b238548d7d42a72773a", + "reset_restored_seed": true, + "final_url_path": "/list/watchlist" + }, + { + "task_id": "IMDb--16", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 11, + "read_only_database_unchanged": false, + "after_db_sha256": "b3004d7ffb8fb91f57a497facb29f10ad7aede480a95338716d71b9a83cf12a1", + "reset_restored_seed": true, + "final_url_path": "/list/ratings" + }, + { + "task_id": "IMDb--17", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 14, + "read_only_database_unchanged": false, + "after_db_sha256": "001d8c5007a7f5539483a5cd96ce20b9e326c528d078fba91e6c43051803adcf", + "reset_restored_seed": true, + "final_url_path": "/title/tt0816692/reviews" + }, + { + "task_id": "IMDb--18", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 13, + "read_only_database_unchanged": true, + "after_db_sha256": "9d843c5388ecbc0d5265ac316cdaee915f8b203121b5a5330d207af5fb6206b0", + "reset_restored_seed": true, + "final_url_path": "/search/title?title_type=movie&year_from=&year_to=2010&rating_min=&genre=animation&sort=rating" + }, + { + "task_id": "IMDb--19", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 11, + "read_only_database_unchanged": true, + "after_db_sha256": "9d843c5388ecbc0d5265ac316cdaee915f8b203121b5a5330d207af5fb6206b0", + "reset_restored_seed": true, + "final_url_path": "/name/nm0000158" + }, + { + "task_id": "IMDb--20", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 16, + "read_only_database_unchanged": true, + "after_db_sha256": "9d843c5388ecbc0d5265ac316cdaee915f8b203121b5a5330d207af5fb6206b0", + "reset_restored_seed": true, + "final_url_path": "/name/nm0000138" + }, + { + "task_id": "IMDb--21", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 29, + "read_only_database_unchanged": true, + "after_db_sha256": "9d843c5388ecbc0d5265ac316cdaee915f8b203121b5a5330d207af5fb6206b0", + "reset_restored_seed": true, + "final_url_path": "/title/tt0120737" + }, + { + "task_id": "IMDb--22", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 35, + "read_only_database_unchanged": true, + "after_db_sha256": "9d843c5388ecbc0d5265ac316cdaee915f8b203121b5a5330d207af5fb6206b0", + "reset_restored_seed": true, + "final_url_path": "/title/tt0816692/reviews" + }, + { + "task_id": "IMDb--23", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 15, + "read_only_database_unchanged": true, + "after_db_sha256": "9d843c5388ecbc0d5265ac316cdaee915f8b203121b5a5330d207af5fb6206b0", + "reset_restored_seed": true, + "final_url_path": "/title/tt0109830" + }, + { + "task_id": "IMDb--24", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 20, + "read_only_database_unchanged": false, + "after_db_sha256": "de4c922c62a6ba52998c462413e15b3fd3bb012a5b2ca60b9e6b88f3152cd31e", + "reset_restored_seed": true, + "final_url_path": "/list/watchlist" + }, + { + "task_id": "IMDb--25", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 23, + "read_only_database_unchanged": false, + "after_db_sha256": "b2d1d26492932204aee9b6448ab0de40fb21bc23762e0f627b9829e352ae6000", + "reset_restored_seed": true, + "final_url_path": "/list/watchlist" + }, + { + "task_id": "IMDb--26", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 30, + "read_only_database_unchanged": false, + "after_db_sha256": "f65994272415143b0f2ea7c91d1cffeefce9d8fa1e0ea03143dd825a65f5a9f1", + "reset_restored_seed": true, + "final_url_path": "/list/watchlist" + }, + { + "task_id": "IMDb--27", + "status": "PASS", + "ui_pass": true, + "verifier_pass": true, + "steps": 22, + "read_only_database_unchanged": false, + "after_db_sha256": "e4b1cab41b9673cfdf9aaf3890f6ae79036a0240c2b1d0d4b759dbd7e6324542", + "reset_restored_seed": true, + "final_url_path": "/list/ratings" + } + ] + }, + "browser": { + "created_at": "2026-09-12T03:30:11.492651Z", + "result": "PASS", + "source_basis": { + "url": "https://www.imdb.com/most-anticipated/this-month/", + "fresh_capture": false, + "reason": "Anonymous Chromium receives IMDb 403; source fields and Owner acceptance were frozen before publication." + }, + "viewports": { + "desktop-1440": { + "viewport": { + "width": 1440, + "height": 900 + }, + "homepage": { + "status": 200, + "overflow": false, + "images": 210, + "images_loaded": 115 + }, + "feature": { + "status": 200, + "items": 17, + "images": 20, + "overflow": false + }, + "detail_routes": 17, + "detail_routes_200": 17, + "detail_overflow": false + }, + "mobile-390": { + "viewport": { + "width": 390, + "height": 844 + }, + "homepage": { + "status": 200, + "overflow": false, + "images": 210, + "images_loaded": 94 + }, + "feature": { + "status": 200, + "items": 17, + "images": 20, + "overflow": false + }, + "detail_routes": 17, + "detail_routes_200": 17, + "detail_overflow": false + } + }, + "source_full_page_png_sha256": { + "desktop-1440": { + "homepage": "6a91b1fefb050e7dfd8a12cc97b10b88c73205b1d5d44cea368f5458e91b2389", + "feature": "134b6ed21eadaff2f9e335cb6e1051127c48ae71b55e0114d3e892d211296492" + }, + "mobile-390": { + "homepage": "1840334d1fcae789b7ab0feaefd0f4b78c73e05689e3531d3d3466ba7455eecd", + "feature": "936dbf30557356fd32fcc754b8c3638a8b036693afdaa2c58d01defeb21f6611" + } + }, + "public_screenshots": [ + "round4-home-1440.jpg", + "round4-feature-1440.jpg", + "round4-home-390.jpg", + "round4-feature-detail-1440.jpg" + ] + }, + "owner_visual_acceptance": "ACCEPTED_2026-09-12 (Owner compared the expanded homepage/feature result with live IMDb)" +} diff --git a/review-reports/assets/pr33-imdb/csrf-rejected.png b/review-reports/assets/pr33-imdb/csrf-rejected.png new file mode 100644 index 00000000..353fa8b2 Binary files /dev/null and b/review-reports/assets/pr33-imdb/csrf-rejected.png differ diff --git a/review-reports/assets/pr33-imdb/engineering-tests-final.txt b/review-reports/assets/pr33-imdb/engineering-tests-final.txt new file mode 100644 index 00000000..93b6360b --- /dev/null +++ b/review-reports/assets/pr33-imdb/engineering-tests-final.txt @@ -0,0 +1,185 @@ +test_dates_and_negation (test_answer_checks.AnswerChecksTests.test_dates_and_negation) ... ok +test_equivalent_units_and_exact_value (test_answer_checks.AnswerChecksTests.test_equivalent_units_and_exact_value) ... ok +test_field_binding_in_either_direction (test_answer_checks.AnswerChecksTests.test_field_binding_in_either_direction) ... ok +test_rows_and_columns_bind_entities (test_answer_checks.AnswerChecksTests.test_rows_and_columns_bind_entities) ... ok +test_sentence_final_number_and_decimal_boundaries (test_answer_checks.AnswerChecksTests.test_sentence_final_number_and_decimal_boundaries) ... ok +test_year_does_not_masquerade_as_money (test_answer_checks.AnswerChecksTests.test_year_does_not_masquerade_as_money) ... ok +test_invalid_review_ratings_do_not_write (test_app.ReviewInputTests.test_invalid_review_ratings_do_not_write) ... /tmp/imdb-handler-tests-nygb_phi/app.py:320: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + 'now_year': datetime.utcnow().year, +ok +test_valid_optional_and_boundary_review_ratings (test_app.ReviewInputTests.test_valid_optional_and_boundary_review_ratings) ... /usr/local/lib/python3.12/site-packages/sqlalchemy/sql/schema.py:3596: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + return util.wrap_callable(lambda ctx: fn(), fn) # type: ignore +ok +test_account_body_logout_form_ends_session_without_data_changes (test_boundary.BoundaryTests.test_account_body_logout_form_ends_session_without_data_changes) ... /tmp/imdb-synthetic-boundary-tests-5rspr3_5/app.py:320: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + 'now_year': datetime.utcnow().year, +ok +test_all_post_routes_reject_missing_and_invalid_csrf (test_boundary.BoundaryTests.test_all_post_routes_reject_missing_and_invalid_csrf) ... ok +test_blank_and_supported_years_preserve_search (test_boundary.BoundaryTests.test_blank_and_supported_years_preserve_search) ... ok +test_cross_origin_without_token_cannot_mutate (test_boundary.BoundaryTests.test_cross_origin_without_token_cannot_mutate) ... ok +test_duplicate_registration_and_bad_login_remain_rejected (test_boundary.BoundaryTests.test_duplicate_registration_and_bad_login_remain_rejected) ... ok +test_invalid_years_return_clear_400_without_database_changes (test_boundary.BoundaryTests.test_invalid_years_return_clear_400_without_database_changes) ... ok +test_login_and_registration_without_session_reject_missing_csrf (test_boundary.BoundaryTests.test_login_and_registration_without_session_reject_missing_csrf) ... ok +test_logout_requires_token_post_and_get_head_preserve_session (test_boundary.BoundaryTests.test_logout_requires_token_post_and_get_head_preserve_session) ... ok +test_other_session_token_is_rejected (test_boundary.BoundaryTests.test_other_session_token_is_rejected) ... ok +test_real_form_tokens_allow_normal_writes_and_preserve_ownership (test_boundary.BoundaryTests.test_real_form_tokens_allow_normal_writes_and_preserve_ownership) ... ok +test_referrer_default_ports_and_local_ipv6_are_parsed (test_boundary.BoundaryTests.test_referrer_default_ports_and_local_ipv6_are_parsed) ... ok +test_registration_email_validation_is_offline_and_normalized (test_boundary.BoundaryTests.test_registration_email_validation_is_offline_and_normalized) ... ok +test_token_expiry_remains_enabled (test_boundary.BoundaryTests.test_token_expiry_remains_enabled) ... ok +test_watchlist_referrers_preserve_only_same_origin_path_query (test_boundary.BoundaryTests.test_watchlist_referrers_preserve_only_same_origin_path_query) ... ok +test_another_money_field_cannot_supply_the_budget (test_read_tasks.SyntheticReadTaskTests.test_another_money_field_cannot_supply_the_budget) ... ok +test_catalog_changes_drive_truth_instead_of_hardcoded_answers (test_read_tasks.SyntheticReadTaskTests.test_catalog_changes_drive_truth_instead_of_hardcoded_answers) ... ok +test_comparison_in_second_clause_keeps_its_own_subject (test_read_tasks.SyntheticReadTaskTests.test_comparison_in_second_clause_keeps_its_own_subject) ... ok +test_comparison_winner_table_and_missing_conclusion (test_read_tasks.SyntheticReadTaskTests.test_comparison_winner_table_and_missing_conclusion) ... ok +test_correct_negative_tie_explanation_is_allowed (test_read_tasks.SyntheticReadTaskTests.test_correct_negative_tie_explanation_is_allowed) ... ok +test_director_full_names_do_not_match_unrelated_surnames (test_read_tasks.SyntheticReadTaskTests.test_director_full_names_do_not_match_unrelated_surnames) ... ok +test_director_rating_can_come_from_other_public_listings (test_read_tasks.SyntheticReadTaskTests.test_director_rating_can_come_from_other_public_listings) ... ok +test_empty_advanced_results_do_not_prove_a_director_movie_rating (test_read_tasks.SyntheticReadTaskTests.test_empty_advanced_results_do_not_prove_a_director_movie_rating) ... ok +test_every_read_task_rejects_business_state_changes (test_read_tasks.SyntheticReadTaskTests.test_every_read_task_rejects_business_state_changes) ... ok +test_every_read_task_requires_relevant_navigation (test_read_tasks.SyntheticReadTaskTests.test_every_read_task_requires_relevant_navigation) ... ok +test_explicit_director_assertions_reject_extra_people (test_read_tasks.SyntheticReadTaskTests.test_explicit_director_assertions_reject_extra_people) ... ok +test_extra_false_rating_tie_is_not_accepted (test_read_tasks.SyntheticReadTaskTests.test_extra_false_rating_tie_is_not_accepted) ... ok +test_fields_on_another_title_cannot_complete_the_winner (test_read_tasks.SyntheticReadTaskTests.test_fields_on_another_title_cannot_complete_the_winner) ... ok +test_filter_duplicate_and_equivalent_query_values (test_read_tasks.SyntheticReadTaskTests.test_filter_duplicate_and_equivalent_query_values) ... ok +test_foreign_urls_cannot_supply_the_required_navigation (test_read_tasks.SyntheticReadTaskTests.test_foreign_urls_cannot_supply_the_required_navigation) ... ok +test_highest_result_sets_reject_extra_winners_not_loser_explanations (test_read_tasks.SyntheticReadTaskTests.test_highest_result_sets_reject_extra_winners_not_loser_explanations) ... ok +test_markdown_money_columns_and_explicit_comparisons (test_read_tasks.SyntheticReadTaskTests.test_markdown_money_columns_and_explicit_comparisons) ... ok +test_markdown_rows_preserve_runtime_and_classification_binding (test_read_tasks.SyntheticReadTaskTests.test_markdown_rows_preserve_runtime_and_classification_binding) ... ok +test_money_accepts_exact_amounts_and_equivalent_units (test_read_tasks.SyntheticReadTaskTests.test_money_accepts_exact_amounts_and_equivalent_units) ... ok +test_multi_line_fact_blocks_and_group_headings (test_read_tasks.SyntheticReadTaskTests.test_multi_line_fact_blocks_and_group_headings) ... ok +test_task0_difference_is_distinct_from_runtime_and_must_be_correct (test_read_tasks.SyntheticReadTaskTests.test_task0_difference_is_distinct_from_runtime_and_must_be_correct) ... ok +test_task0_explicit_word_ranks_keep_their_title_binding (test_read_tasks.SyntheticReadTaskTests.test_task0_explicit_word_ranks_keep_their_title_binding) ... ok +test_task0_genuine_single_line_answer_with_runtime_difference (test_read_tasks.SyntheticReadTaskTests.test_task0_genuine_single_line_answer_with_runtime_difference) ... ok +test_task0_prefix_ranks_bind_forward_in_single_and_multiple_lines (test_read_tasks.SyntheticReadTaskTests.test_task0_prefix_ranks_bind_forward_in_single_and_multiple_lines) ... ok +test_task0_ranked_films_can_be_visited_in_either_order (test_read_tasks.SyntheticReadTaskTests.test_task0_ranked_films_can_be_visited_in_either_order) ... ok +test_task0_requires_both_title_details_and_chart (test_read_tasks.SyntheticReadTaskTests.test_task0_requires_both_title_details_and_chart) ... ok +test_task0_reversed_runtimes_are_not_rescued_by_correct_difference (test_read_tasks.SyntheticReadTaskTests.test_task0_reversed_runtimes_are_not_rescued_by_correct_difference) ... ok +test_task0_suffix_and_mixed_rank_positions_still_bind_correctly (test_read_tasks.SyntheticReadTaskTests.test_task0_suffix_and_mixed_rank_positions_still_bind_correctly) ... ok +test_task10_all_directors_must_be_reported (test_read_tasks.SyntheticReadTaskTests.test_task10_all_directors_must_be_reported) ... ok +test_task10_all_search_constraints_are_required (test_read_tasks.SyntheticReadTaskTests.test_task10_all_search_constraints_are_required) ... ok +test_task10_crime_nineties_director_and_worldwide (test_read_tasks.SyntheticReadTaskTests.test_task10_crime_nineties_director_and_worldwide) ... ok +test_task10_inclusive_year_endpoints_and_all_top_ties (test_read_tasks.SyntheticReadTaskTests.test_task10_inclusive_year_endpoints_and_all_top_ties) ... ok +test_task12_all_within_group_ties_and_tied_comparison (test_read_tasks.SyntheticReadTaskTests.test_task12_all_within_group_ties_and_tied_comparison) ... ok +test_task12_compares_movie_and_tv_top_ratings (test_read_tasks.SyntheticReadTaskTests.test_task12_compares_movie_and_tv_top_ratings) ... ok +test_task12_inline_extra_winner_and_wrong_comparison_are_rejected (test_read_tasks.SyntheticReadTaskTests.test_task12_inline_extra_winner_and_wrong_comparison_are_rejected) ... ok +test_task12_reversed_type_prefixes_are_not_rescued_by_correct_values (test_read_tasks.SyntheticReadTaskTests.test_task12_reversed_type_prefixes_are_not_rescued_by_correct_values) ... ok +test_task12_single_line_type_prefixes_stay_with_their_movies (test_read_tasks.SyntheticReadTaskTests.test_task12_single_line_type_prefixes_stay_with_their_movies) ... ok +test_task12_wrong_comparison_cannot_hide_behind_correct_values (test_read_tasks.SyntheticReadTaskTests.test_task12_wrong_comparison_cannot_hide_behind_correct_values) ... ok +test_task14_ties_are_reported_explicitly (test_read_tasks.SyntheticReadTaskTests.test_task14_ties_are_reported_explicitly) ... ok +test_task14_values_and_both_comparisons_in_either_visit_order (test_read_tasks.SyntheticReadTaskTests.test_task14_values_and_both_comparisons_in_either_visit_order) ... ok +test_task2_uses_catalog_cumulative_domestic_gross (test_read_tasks.SyntheticReadTaskTests.test_task2_uses_catalog_cumulative_domestic_gross) ... ok +test_task7_all_director_movie_rating_ties (test_read_tasks.SyntheticReadTaskTests.test_task7_all_director_movie_rating_ties) ... ok +test_task7_director_movies_exclude_writer_credit_and_tv (test_read_tasks.SyntheticReadTaskTests.test_task7_director_movies_exclude_writer_credit_and_tv) ... ok +test_task9_any_third_place_tie_selection_keeps_all_higher_titles (test_read_tasks.SyntheticReadTaskTests.test_task9_any_third_place_tie_selection_keeps_all_higher_titles) ... ok +test_task9_filters_must_all_be_present_and_correct (test_read_tasks.SyntheticReadTaskTests.test_task9_filters_must_all_be_present_and_correct) ... ok +test_task9_order_within_an_included_tie_is_irrelevant (test_read_tasks.SyntheticReadTaskTests.test_task9_order_within_an_included_tie_is_irrelevant) ... ok +test_task9_requires_exactly_three_in_descending_order (test_read_tasks.SyntheticReadTaskTests.test_task9_requires_exactly_three_in_descending_order) ... ok +test_task9_top_three_from_results_page (test_read_tasks.SyntheticReadTaskTests.test_task9_top_three_from_results_page) ... ok +test_wrong_or_partial_answers_are_rejected (test_read_tasks.SyntheticReadTaskTests.test_wrong_or_partial_answers_are_rejected) ... ok +test_migration_preserves_relationships_and_unrelated_state (test_seed_data.SeedMigrationTests.test_migration_preserves_relationships_and_unrelated_state) ... ok +test_original_source_combines_date_and_canonical_year_corrections (test_seed_data.SeedMigrationTests.test_original_source_combines_date_and_canonical_year_corrections) ... ok +test_previous_candidate_changes_only_two_year_fields (test_seed_data.SeedMigrationTests.test_previous_candidate_changes_only_two_year_fields) ... ok +test_second_migration_is_byte_identical (test_seed_data.SeedMigrationTests.test_second_migration_is_byte_identical) ... ok +test_unexpected_source_is_rejected_without_writes (test_seed_data.SeedMigrationTests.test_unexpected_source_is_rejected_without_writes) ... ok +test_unexpected_title_year_is_rejected_without_writes (test_seed_data.SeedMigrationTests.test_unexpected_title_year_is_rejected_without_writes) ... ok +test_complete_dates_only_and_structured_source_wins (test_seed_data.SourceParsingTests.test_complete_dates_only_and_structured_source_wins) ... ok +test_person_canonical_url_is_an_exact_identity (test_seed_data.SourceParsingTests.test_person_canonical_url_is_an_exact_identity) ... ok +test_seed_rejects_wrong_missing_and_malformed_person_identity (test_seed_data.SourceParsingTests.test_seed_rejects_wrong_missing_and_malformed_person_identity) ... ok +test_abandoned_form_inputs_do_not_override_a_reopened_native_form (test_state_tasks.SyntheticStateTaskTests.test_abandoned_form_inputs_do_not_override_a_reopened_native_form) ... ok +test_answers_must_report_the_selected_target_and_result (test_state_tasks.SyntheticStateTaskTests.test_answers_must_report_the_selected_target_and_result) ... ok +test_control_metadata_outside_locator_child_is_not_an_action_description (test_state_tasks.SyntheticStateTaskTests.test_control_metadata_outside_locator_child_is_not_an_action_description) ... ok +test_correct_delta_requires_login_with_the_requested_account (test_state_tasks.SyntheticStateTaskTests.test_correct_delta_requires_login_with_the_requested_account) ... ok +test_each_task_rejects_changes_to_unrelated_business_tables (test_state_tasks.SyntheticStateTaskTests.test_each_task_rejects_changes_to_unrelated_business_tables) ... ok +test_enter_can_submit_login (test_state_tasks.SyntheticStateTaskTests.test_enter_can_submit_login) ... ok +test_explicit_cancel_text_does_not_count_as_a_submission (test_state_tasks.SyntheticStateTaskTests.test_explicit_cancel_text_does_not_count_as_a_submission) ... ok +test_explicit_conflicting_form_values_are_rejected (test_state_tasks.SyntheticStateTaskTests.test_explicit_conflicting_form_values_are_rejected) ... ok +test_explicit_failed_mutation_is_not_rescued_by_a_plausible_delta (test_state_tasks.SyntheticStateTaskTests.test_explicit_failed_mutation_is_not_rescued_by_a_plausible_delta) ... ok +test_explicit_success_after_url_can_confirm_the_review (test_state_tasks.SyntheticStateTaskTests.test_explicit_success_after_url_can_confirm_the_review) ... ok +test_explicit_wrong_target_controls_are_rejected (test_state_tasks.SyntheticStateTaskTests.test_explicit_wrong_target_controls_are_rejected) ... ok +test_foreign_login_and_mutation_urls_do_not_count (test_state_tasks.SyntheticStateTaskTests.test_foreign_login_and_mutation_urls_do_not_count) ... ok +test_get_and_head_logout_405_observations_preserve_login (test_state_tasks.SyntheticStateTaskTests.test_get_and_head_logout_405_observations_preserve_login) ... ok +test_last_corrected_form_value_is_used (test_state_tasks.SyntheticStateTaskTests.test_last_corrected_form_value_is_used) ... ok +test_native_index_click_with_unknown_success_uses_later_page_and_delta (test_state_tasks.SyntheticStateTaskTests.test_native_index_click_with_unknown_success_uses_later_page_and_delta) ... ok +test_native_unknown_login_and_enter_submission_are_supported (test_state_tasks.SyntheticStateTaskTests.test_native_unknown_login_and_enter_submission_are_supported) ... ok +test_native_unknown_logout_needs_a_later_local_observation (test_state_tasks.SyntheticStateTaskTests.test_native_unknown_logout_needs_a_later_local_observation) ... ok +test_nested_action_cue_must_describe_child_not_parent_container (test_state_tasks.SyntheticStateTaskTests.test_nested_action_cue_must_describe_child_not_parent_container) ... ok +test_nested_opaque_index_preserves_target_constraints (test_state_tasks.SyntheticStateTaskTests.test_nested_opaque_index_preserves_target_constraints) ... ok +test_noop_is_not_success_even_if_answer_and_steps_claim_success (test_state_tasks.SyntheticStateTaskTests.test_noop_is_not_success_even_if_answer_and_steps_claim_success) ... ok +test_obsolete_logout_get_link_does_not_clear_login (test_state_tasks.SyntheticStateTaskTests.test_obsolete_logout_get_link_does_not_clear_login) ... ok +test_personal_and_review_ratings_are_distinct_from_imdb_context (test_state_tasks.SyntheticStateTaskTests.test_personal_and_review_ratings_are_distinct_from_imdb_context) ... ok +test_post_logout_button_or_enter_clears_the_previous_login (test_state_tasks.SyntheticStateTaskTests.test_post_logout_button_or_enter_clears_the_previous_login) ... ok +test_private_confirmation_cannot_belong_to_a_different_account (test_state_tasks.SyntheticStateTaskTests.test_private_confirmation_cannot_belong_to_a_different_account) ... ok +test_pure_navigation_does_not_prove_a_mutation_action (test_state_tasks.SyntheticStateTaskTests.test_pure_navigation_does_not_prove_a_mutation_action) ... ok +test_required_candidate_and_confirmation_pages_cannot_be_omitted (test_state_tasks.SyntheticStateTaskTests.test_required_candidate_and_confirmation_pages_cannot_be_omitted) ... ok +test_review_confirmation_can_be_concise_but_cannot_claim_a_wrong_headline (test_state_tasks.SyntheticStateTaskTests.test_review_confirmation_can_be_concise_but_cannot_claim_a_wrong_headline) ... ok +test_task15_either_qualifying_genre_is_sufficient (test_state_tasks.SyntheticStateTaskTests.test_task15_either_qualifying_genre_is_sufficient) ... ok +test_task15_nested_button_does_not_rescue_noop_or_extra_write (test_state_tasks.SyntheticStateTaskTests.test_task15_nested_button_does_not_rescue_noop_or_extra_write) ... ok +test_task15_nested_child_must_describe_the_removal_control (test_state_tasks.SyntheticStateTaskTests.test_task15_nested_child_must_describe_the_removal_control) ... ok +test_task15_nested_locator_keeps_parent_title_and_origin_constraints (test_state_tasks.SyntheticStateTaskTests.test_task15_nested_locator_keeps_parent_title_and_origin_constraints) ... ok +test_task15_nested_watchlist_row_remove_button_is_valid (test_state_tasks.SyntheticStateTaskTests.test_task15_nested_watchlist_row_remove_button_is_valid) ... ok +test_task15_removes_earliest_qualifying_tv_with_alphabetical_tie_break (test_state_tasks.SyntheticStateTaskTests.test_task15_removes_earliest_qualifying_tv_with_alphabetical_tie_break) ... ok +test_task15_removing_an_additional_watchlist_item_is_rejected (test_state_tasks.SyntheticStateTaskTests.test_task15_removing_an_additional_watchlist_item_is_rejected) ... ok +test_task15_reports_both_qualifying_genres_when_both_present (test_state_tasks.SyntheticStateTaskTests.test_task15_reports_both_qualifying_genres_when_both_present) ... ok +test_task15_target_title_removal_and_confirmation_are_valid (test_state_tasks.SyntheticStateTaskTests.test_task15_target_title_removal_and_confirmation_are_valid) ... ok +test_task15_wrong_tie_choice_type_or_genre_is_rejected (test_state_tasks.SyntheticStateTaskTests.test_task15_wrong_tie_choice_type_or_genre_is_rejected) ... ok +test_task16_another_users_rating_or_global_rating_cannot_change (test_state_tasks.SyntheticStateTaskTests.test_task16_another_users_rating_or_global_rating_cannot_change) ... ok +test_task16_correct_history_cannot_rescue_extra_database_writes (test_state_tasks.SyntheticStateTaskTests.test_task16_correct_history_cannot_rescue_extra_database_writes) ... ok +test_task16_distinguishes_verified_previous_and_saved_ratings (test_state_tasks.SyntheticStateTaskTests.test_task16_distinguishes_verified_previous_and_saved_ratings) ... ok +test_task16_eligibility_or_history_alone_does_not_confirm_eight (test_state_tasks.SyntheticStateTaskTests.test_task16_eligibility_or_history_alone_does_not_confirm_eight) ... ok +test_task16_existing_rating_keeps_id_and_created_at (test_state_tasks.SyntheticStateTaskTests.test_task16_existing_rating_keeps_id_and_created_at) ... ok +test_task16_from_release_year_is_not_a_previous_personal_rating (test_state_tasks.SyntheticStateTaskTests.test_task16_from_release_year_is_not_a_previous_personal_rating) ... ok +test_task16_incorrect_current_rating_is_not_masked_by_history (test_state_tasks.SyntheticStateTaskTests.test_task16_incorrect_current_rating_is_not_masked_by_history) ... ok +test_task16_incorrect_previous_rating_is_rejected (test_state_tasks.SyntheticStateTaskTests.test_task16_incorrect_previous_rating_is_rejected) ... ok +test_task16_newer_but_already_eight_and_nonmovie_or_nongenre_are_excluded (test_state_tasks.SyntheticStateTaskTests.test_task16_newer_but_already_eight_and_nonmovie_or_nongenre_are_excluded) ... ok +test_task16_null_personal_rating_is_eligible (test_state_tasks.SyntheticStateTaskTests.test_task16_null_personal_rating_is_eligible) ... ok +test_task16_rating_from_and_real_from_title_keep_separate_bindings (test_state_tasks.SyntheticStateTaskTests.test_task16_rating_from_and_real_from_title_keep_separate_bindings) ... ok +test_task16_rating_transition_is_not_the_catalog_title_from (test_state_tasks.SyntheticStateTaskTests.test_task16_rating_transition_is_not_the_catalog_title_from) ... ok +test_task16_real_answer_structure_binds_old_value_to_initial_snapshot (test_state_tasks.SyntheticStateTaskTests.test_task16_real_answer_structure_binds_old_value_to_initial_snapshot) ... ok +test_task16_unrated_initial_state_cannot_claim_a_previous_numeric_rating (test_state_tasks.SyntheticStateTaskTests.test_task16_unrated_initial_state_cannot_claim_a_previous_numeric_rating) ... ok +test_task16_unrated_target_gets_one_new_rating (test_state_tasks.SyntheticStateTaskTests.test_task16_unrated_target_gets_one_new_rating) ... ok +test_task16_updates_only_existing_target_rating (test_state_tasks.SyntheticStateTaskTests.test_task16_updates_only_existing_target_rating) ... ok +test_task17_any_review_sort_and_direct_or_reviews_entry_are_valid (test_state_tasks.SyntheticStateTaskTests.test_task17_any_review_sort_and_direct_or_reviews_entry_are_valid) ... ok +test_task17_created_review_fields_must_match (test_state_tasks.SyntheticStateTaskTests.test_task17_created_review_fields_must_match) ... ok +test_task17_creates_exactly_one_review_without_personal_rating_write (test_state_tasks.SyntheticStateTaskTests.test_task17_creates_exactly_one_review_without_personal_rating_write) ... ok +test_task17_explicit_wrong_headline_cannot_hide_behind_a_reference (test_state_tasks.SyntheticStateTaskTests.test_task17_explicit_wrong_headline_cannot_hide_behind_a_reference) ... ok +test_task17_extra_review_or_rating_write_is_rejected (test_state_tasks.SyntheticStateTaskTests.test_task17_extra_review_or_rating_write_is_rejected) ... ok +test_task17_headline_reference_does_not_override_contradictory_rating (test_state_tasks.SyntheticStateTaskTests.test_task17_headline_reference_does_not_override_contradictory_rating) ... ok +test_task17_headline_reference_is_not_an_additional_literal_value (test_state_tasks.SyntheticStateTaskTests.test_task17_headline_reference_is_not_an_additional_literal_value) ... ok +test_task17_headline_value_can_precede_identifiable_metadata (test_state_tasks.SyntheticStateTaskTests.test_task17_headline_value_can_precede_identifiable_metadata) ... ok +test_task17_new_review_body_can_be_any_nonempty_text (test_state_tasks.SyntheticStateTaskTests.test_task17_new_review_body_can_be_any_nonempty_text) ... ok +test_task17_pilot_answer_with_later_headline_reference (test_state_tasks.SyntheticStateTaskTests.test_task17_pilot_answer_with_later_headline_reference) ... ok +test_task17_quoted_extra_headline_suffix_is_not_metadata (test_state_tasks.SyntheticStateTaskTests.test_task17_quoted_extra_headline_suffix_is_not_metadata) ... ok +test_task17_unquoted_extra_headline_suffix_is_not_metadata (test_state_tasks.SyntheticStateTaskTests.test_task17_unquoted_extra_headline_suffix_is_not_metadata) ... ok +test_unknown_after_url_alone_does_not_confirm_mutation (test_state_tasks.SyntheticStateTaskTests.test_unknown_after_url_alone_does_not_confirm_mutation) ... ok +test_watchlist_candidate_page_must_belong_to_the_requested_account (test_state_tasks.SyntheticStateTaskTests.test_watchlist_candidate_page_must_belong_to_the_requested_account) ... ok +test_current_tasks_and_numbered_entries_are_one_to_one (test_verify_entry.VerifierEntryTests.test_current_tasks_and_numbered_entries_are_one_to_one) ... ok +test_every_task_entry_rejects_another_task_or_missing_artifacts (test_verify_entry.VerifierEntryTests.test_every_task_entry_rejects_another_task_or_missing_artifacts) ... ok +test_missing_after_snapshot_never_falls_back_to_live_database (test_verify_entry.VerifierEntryTests.test_missing_after_snapshot_never_falls_back_to_live_database) ... ok +test_only_absolute_run_dir_argument_works_from_another_cwd (test_verify_entry.VerifierEntryTests.test_only_absolute_run_dir_argument_works_from_another_cwd) ... ok +test_same_id_old_question_is_not_current_candidate_evidence (test_verify_entry.VerifierEntryTests.test_same_id_old_question_is_not_current_candidate_evidence) ... ok +test_cli_arguments (test_verify_lib.SyntheticRunTests.test_cli_arguments) ... ok +test_connections_are_readonly_rows_and_closed (test_verify_lib.SyntheticRunTests.test_connections_are_readonly_rows_and_closed) ... ok +test_default_http_port_and_hostname_case_are_equivalent (test_verify_lib.SyntheticRunTests.test_default_http_port_and_hostname_case_are_equivalent) ... ok +test_emit_result_is_one_json_object_and_exit_status (test_verify_lib.SyntheticRunTests.test_emit_result_is_one_json_object_and_exit_status) ... ok +test_empty_business_table_added_removed_or_schema_changed (test_verify_lib.SyntheticRunTests.test_empty_business_table_added_removed_or_schema_changed) ... ok +test_failed_after_url_does_not_prove_navigation (test_verify_lib.SyntheticRunTests.test_failed_after_url_does_not_prove_navigation) ... ok +test_failure_overrides_other_success_flags_and_unknown_is_not_success (test_verify_lib.SyntheticRunTests.test_failure_overrides_other_success_flags_and_unknown_is_not_success) ... ok +test_foreign_events_same_path_and_embedded_path_do_not_count (test_verify_lib.SyntheticRunTests.test_foreign_events_same_path_and_embedded_path_do_not_count) ... ok +test_foreign_start_urls_rejected (test_verify_lib.SyntheticRunTests.test_foreign_start_urls_rejected) ... ok +test_initial_filename_and_explicit_overrides (test_verify_lib.SyntheticRunTests.test_initial_filename_and_explicit_overrides) ... ok +test_local_start_hosts_and_runtime_port_override (test_verify_lib.SyntheticRunTests.test_local_start_hosts_and_runtime_port_override) ... ok +test_malformed_success_status_does_not_crash_or_prove_navigation (test_verify_lib.SyntheticRunTests.test_malformed_success_status_does_not_crash_or_prove_navigation) ... ok +test_missing_snapshots_fail_without_live_fallback (test_verify_lib.SyntheticRunTests.test_missing_snapshots_fail_without_live_fallback) ... ok +test_multiset_change_and_extra_table_write_detected_without_secret_log (test_verify_lib.SyntheticRunTests.test_multiset_change_and_extra_table_write_detected_without_secret_log) ... ok +test_ordered_visits_require_each_event_but_no_implicit_home (test_verify_lib.SyntheticRunTests.test_ordered_visits_require_each_event_but_no_implicit_home) ... ok +test_question_whitespace_only_normalization_and_final_answer (test_verify_lib.SyntheticRunTests.test_question_whitespace_only_normalization_and_final_answer) ... ok +test_reordered_rows_are_unchanged (test_verify_lib.SyntheticRunTests.test_reordered_rows_are_unchanged) ... ok +test_sqlite_internal_tables_are_ignored (test_verify_lib.SyntheticRunTests.test_sqlite_internal_tables_are_ignored) ... ok +test_start_url_alone_is_not_visit_evidence (test_verify_lib.SyntheticRunTests.test_start_url_alone_is_not_visit_evidence) ... ok +test_storage_types_and_null_are_not_stringified (test_verify_lib.SyntheticRunTests.test_storage_types_and_null_are_not_stringified) ... ok +test_tail_slash_query_subsets_and_decoding (test_verify_lib.SyntheticRunTests.test_tail_slash_query_subsets_and_decoding) ... ok +test_wrong_task_replay_and_question_replay_rejected (test_verify_lib.SyntheticRunTests.test_wrong_task_replay_and_question_replay_rejected) ... ok + +---------------------------------------------------------------------- +Ran 174 tests in 16.020s + +OK diff --git a/review-reports/assets/pr33-imdb/engineering-tests.txt b/review-reports/assets/pr33-imdb/engineering-tests.txt new file mode 100644 index 00000000..1c6f3882 --- /dev/null +++ b/review-reports/assets/pr33-imdb/engineering-tests.txt @@ -0,0 +1,157 @@ +test_dates_and_negation (test_answer_checks.AnswerChecksTests.test_dates_and_negation) ... ok +test_equivalent_units_and_exact_value (test_answer_checks.AnswerChecksTests.test_equivalent_units_and_exact_value) ... ok +test_field_binding_in_either_direction (test_answer_checks.AnswerChecksTests.test_field_binding_in_either_direction) ... ok +test_rows_and_columns_bind_entities (test_answer_checks.AnswerChecksTests.test_rows_and_columns_bind_entities) ... ok +test_sentence_final_number_and_decimal_boundaries (test_answer_checks.AnswerChecksTests.test_sentence_final_number_and_decimal_boundaries) ... ok +test_year_does_not_masquerade_as_money (test_answer_checks.AnswerChecksTests.test_year_does_not_masquerade_as_money) ... ok +test_invalid_review_ratings_do_not_write (test_app.ReviewInputTests.test_invalid_review_ratings_do_not_write) ... /tmp/imdb-handler-tests-677nwnd2/app.py:320: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + 'now_year': datetime.utcnow().year, +ok +test_valid_optional_and_boundary_review_ratings (test_app.ReviewInputTests.test_valid_optional_and_boundary_review_ratings) ... /usr/local/lib/python3.12/site-packages/sqlalchemy/sql/schema.py:3596: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + return util.wrap_callable(lambda ctx: fn(), fn) # type: ignore +ok +test_account_body_logout_form_ends_session_without_data_changes (test_boundary.BoundaryTests.test_account_body_logout_form_ends_session_without_data_changes) ... /tmp/imdb-synthetic-boundary-tests-cg2kilaa/app.py:320: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + 'now_year': datetime.utcnow().year, +ok +test_all_post_routes_reject_missing_and_invalid_csrf (test_boundary.BoundaryTests.test_all_post_routes_reject_missing_and_invalid_csrf) ... ok +test_blank_and_supported_years_preserve_search (test_boundary.BoundaryTests.test_blank_and_supported_years_preserve_search) ... ok +test_cross_origin_without_token_cannot_mutate (test_boundary.BoundaryTests.test_cross_origin_without_token_cannot_mutate) ... ok +test_duplicate_registration_and_bad_login_remain_rejected (test_boundary.BoundaryTests.test_duplicate_registration_and_bad_login_remain_rejected) ... ok +test_invalid_years_return_clear_400_without_database_changes (test_boundary.BoundaryTests.test_invalid_years_return_clear_400_without_database_changes) ... ok +test_login_and_registration_without_session_reject_missing_csrf (test_boundary.BoundaryTests.test_login_and_registration_without_session_reject_missing_csrf) ... ok +test_logout_requires_token_post_and_get_head_preserve_session (test_boundary.BoundaryTests.test_logout_requires_token_post_and_get_head_preserve_session) ... ok +test_other_session_token_is_rejected (test_boundary.BoundaryTests.test_other_session_token_is_rejected) ... ok +test_real_form_tokens_allow_normal_writes_and_preserve_ownership (test_boundary.BoundaryTests.test_real_form_tokens_allow_normal_writes_and_preserve_ownership) ... ok +test_referrer_default_ports_and_local_ipv6_are_parsed (test_boundary.BoundaryTests.test_referrer_default_ports_and_local_ipv6_are_parsed) ... ok +test_registration_email_validation_is_offline_and_normalized (test_boundary.BoundaryTests.test_registration_email_validation_is_offline_and_normalized) ... ok +test_token_expiry_remains_enabled (test_boundary.BoundaryTests.test_token_expiry_remains_enabled) ... ok +test_watchlist_referrers_preserve_only_same_origin_path_query (test_boundary.BoundaryTests.test_watchlist_referrers_preserve_only_same_origin_path_query) ... ok +test_actor_character_and_birth_year_bind_to_the_same_actor (test_read_tasks.SyntheticReadTaskTests.test_actor_character_and_birth_year_bind_to_the_same_actor) ... ok +test_catalog_changes_drive_truth_instead_of_hardcoded_answers (test_read_tasks.SyntheticReadTaskTests.test_catalog_changes_drive_truth_instead_of_hardcoded_answers) ... ok +test_comparison_in_second_clause_keeps_its_own_subject (test_read_tasks.SyntheticReadTaskTests.test_comparison_in_second_clause_keeps_its_own_subject) ... ok +test_comparison_winner_table_and_missing_conclusion (test_read_tasks.SyntheticReadTaskTests.test_comparison_winner_table_and_missing_conclusion) ... ok +test_correct_negative_tie_explanation_is_allowed (test_read_tasks.SyntheticReadTaskTests.test_correct_negative_tie_explanation_is_allowed) ... ok +test_director_full_names_do_not_match_unrelated_surnames (test_read_tasks.SyntheticReadTaskTests.test_director_full_names_do_not_match_unrelated_surnames) ... ok +test_director_rating_can_come_from_other_public_listings (test_read_tasks.SyntheticReadTaskTests.test_director_rating_can_come_from_other_public_listings) ... ok +test_empty_advanced_results_do_not_prove_a_director_movie_rating (test_read_tasks.SyntheticReadTaskTests.test_empty_advanced_results_do_not_prove_a_director_movie_rating) ... ok +test_every_read_task_rejects_business_state_changes (test_read_tasks.SyntheticReadTaskTests.test_every_read_task_rejects_business_state_changes) ... ok +test_every_read_task_requires_relevant_navigation (test_read_tasks.SyntheticReadTaskTests.test_every_read_task_requires_relevant_navigation) ... ok +test_explicit_director_assertions_reject_extra_people (test_read_tasks.SyntheticReadTaskTests.test_explicit_director_assertions_reject_extra_people) ... ok +test_extra_false_rating_tie_is_not_accepted (test_read_tasks.SyntheticReadTaskTests.test_extra_false_rating_tie_is_not_accepted) ... ok +test_fields_on_another_title_cannot_complete_the_winner (test_read_tasks.SyntheticReadTaskTests.test_fields_on_another_title_cannot_complete_the_winner) ... ok +test_filter_duplicate_and_equivalent_query_values (test_read_tasks.SyntheticReadTaskTests.test_filter_duplicate_and_equivalent_query_values) ... ok +test_foreign_urls_cannot_supply_the_required_navigation (test_read_tasks.SyntheticReadTaskTests.test_foreign_urls_cannot_supply_the_required_navigation) ... ok +test_highest_result_sets_reject_extra_winners_not_loser_explanations (test_read_tasks.SyntheticReadTaskTests.test_highest_result_sets_reject_extra_winners_not_loser_explanations) ... ok +test_markdown_money_columns_and_explicit_comparisons (test_read_tasks.SyntheticReadTaskTests.test_markdown_money_columns_and_explicit_comparisons) ... ok +test_markdown_rows_preserve_runtime_and_classification_binding (test_read_tasks.SyntheticReadTaskTests.test_markdown_rows_preserve_runtime_and_classification_binding) ... ok +test_money_accepts_exact_amounts_and_equivalent_units (test_read_tasks.SyntheticReadTaskTests.test_money_accepts_exact_amounts_and_equivalent_units) ... ok +test_money_labels_cannot_be_swapped_or_omitted (test_read_tasks.SyntheticReadTaskTests.test_money_labels_cannot_be_swapped_or_omitted) ... ok +test_multi_line_fact_blocks_and_group_headings (test_read_tasks.SyntheticReadTaskTests.test_multi_line_fact_blocks_and_group_headings) ... ok +test_task0_explicit_word_ranks_keep_their_title_binding (test_read_tasks.SyntheticReadTaskTests.test_task0_explicit_word_ranks_keep_their_title_binding) ... ok +test_task0_ranked_films_can_be_visited_in_either_order (test_read_tasks.SyntheticReadTaskTests.test_task0_ranked_films_can_be_visited_in_either_order) ... ok +test_task0_requires_both_title_details_and_chart (test_read_tasks.SyntheticReadTaskTests.test_task0_requires_both_title_details_and_chart) ... ok +test_task10_all_directors_must_be_reported (test_read_tasks.SyntheticReadTaskTests.test_task10_all_directors_must_be_reported) ... ok +test_task10_all_search_constraints_are_required (test_read_tasks.SyntheticReadTaskTests.test_task10_all_search_constraints_are_required) ... ok +test_task10_crime_nineties_director_and_worldwide (test_read_tasks.SyntheticReadTaskTests.test_task10_crime_nineties_director_and_worldwide) ... ok +test_task10_inclusive_year_endpoints_and_all_top_ties (test_read_tasks.SyntheticReadTaskTests.test_task10_inclusive_year_endpoints_and_all_top_ties) ... ok +test_task12_all_within_group_ties_and_tied_comparison (test_read_tasks.SyntheticReadTaskTests.test_task12_all_within_group_ties_and_tied_comparison) ... ok +test_task12_compares_movie_and_tv_top_ratings (test_read_tasks.SyntheticReadTaskTests.test_task12_compares_movie_and_tv_top_ratings) ... ok +test_task12_wrong_comparison_cannot_hide_behind_correct_values (test_read_tasks.SyntheticReadTaskTests.test_task12_wrong_comparison_cannot_hide_behind_correct_values) ... ok +test_task14_ties_are_reported_explicitly (test_read_tasks.SyntheticReadTaskTests.test_task14_ties_are_reported_explicitly) ... ok +test_task14_values_and_both_comparisons_in_either_visit_order (test_read_tasks.SyntheticReadTaskTests.test_task14_values_and_both_comparisons_in_either_visit_order) ... ok +test_task2_uses_catalog_cumulative_domestic_gross (test_read_tasks.SyntheticReadTaskTests.test_task2_uses_catalog_cumulative_domestic_gross) ... ok +test_task3_budget_and_opening_are_distinct_labeled_values (test_read_tasks.SyntheticReadTaskTests.test_task3_budget_and_opening_are_distinct_labeled_values) ... ok +test_task4_director_and_worldwide_gross (test_read_tasks.SyntheticReadTaskTests.test_task4_director_and_worldwide_gross) ... ok +test_task7_all_director_movie_rating_ties (test_read_tasks.SyntheticReadTaskTests.test_task7_all_director_movie_rating_ties) ... ok +test_task7_director_movies_exclude_writer_credit_and_tv (test_read_tasks.SyntheticReadTaskTests.test_task7_director_movies_exclude_writer_credit_and_tv) ... ok +test_task8_first_billed_cast_and_birth_year (test_read_tasks.SyntheticReadTaskTests.test_task8_first_billed_cast_and_birth_year) ... ok +test_task8_requires_same_actor_profile_and_correct_character (test_read_tasks.SyntheticReadTaskTests.test_task8_requires_same_actor_profile_and_correct_character) ... ok +test_task9_any_third_place_tie_selection_keeps_all_higher_titles (test_read_tasks.SyntheticReadTaskTests.test_task9_any_third_place_tie_selection_keeps_all_higher_titles) ... ok +test_task9_filters_must_all_be_present_and_correct (test_read_tasks.SyntheticReadTaskTests.test_task9_filters_must_all_be_present_and_correct) ... ok +test_task9_order_within_an_included_tie_is_irrelevant (test_read_tasks.SyntheticReadTaskTests.test_task9_order_within_an_included_tie_is_irrelevant) ... ok +test_task9_requires_exactly_three_in_descending_order (test_read_tasks.SyntheticReadTaskTests.test_task9_requires_exactly_three_in_descending_order) ... ok +test_task9_top_three_from_results_page (test_read_tasks.SyntheticReadTaskTests.test_task9_top_three_from_results_page) ... ok +test_wrong_or_partial_answers_are_rejected (test_read_tasks.SyntheticReadTaskTests.test_wrong_or_partial_answers_are_rejected) ... ok +test_migration_preserves_relationships_and_unrelated_state (test_seed_data.SeedMigrationTests.test_migration_preserves_relationships_and_unrelated_state) ... ok +test_original_source_combines_date_and_canonical_year_corrections (test_seed_data.SeedMigrationTests.test_original_source_combines_date_and_canonical_year_corrections) ... ok +test_previous_candidate_changes_only_two_year_fields (test_seed_data.SeedMigrationTests.test_previous_candidate_changes_only_two_year_fields) ... ok +test_second_migration_is_byte_identical (test_seed_data.SeedMigrationTests.test_second_migration_is_byte_identical) ... ok +test_unexpected_source_is_rejected_without_writes (test_seed_data.SeedMigrationTests.test_unexpected_source_is_rejected_without_writes) ... ok +test_unexpected_title_year_is_rejected_without_writes (test_seed_data.SeedMigrationTests.test_unexpected_title_year_is_rejected_without_writes) ... ok +test_complete_dates_only_and_structured_source_wins (test_seed_data.SourceParsingTests.test_complete_dates_only_and_structured_source_wins) ... ok +test_person_canonical_url_is_an_exact_identity (test_seed_data.SourceParsingTests.test_person_canonical_url_is_an_exact_identity) ... ok +test_seed_rejects_wrong_missing_and_malformed_person_identity (test_seed_data.SourceParsingTests.test_seed_rejects_wrong_missing_and_malformed_person_identity) ... ok +test_abandoned_form_inputs_do_not_override_a_reopened_native_form (test_state_tasks.SyntheticStateTaskTests.test_abandoned_form_inputs_do_not_override_a_reopened_native_form) ... ok +test_answers_must_report_the_selected_target_and_result (test_state_tasks.SyntheticStateTaskTests.test_answers_must_report_the_selected_target_and_result) ... ok +test_correct_delta_requires_login_with_the_requested_account (test_state_tasks.SyntheticStateTaskTests.test_correct_delta_requires_login_with_the_requested_account) ... ok +test_each_task_rejects_changes_to_unrelated_business_tables (test_state_tasks.SyntheticStateTaskTests.test_each_task_rejects_changes_to_unrelated_business_tables) ... ok +test_enter_can_submit_login (test_state_tasks.SyntheticStateTaskTests.test_enter_can_submit_login) ... ok +test_explicit_cancel_text_does_not_count_as_a_submission (test_state_tasks.SyntheticStateTaskTests.test_explicit_cancel_text_does_not_count_as_a_submission) ... ok +test_explicit_conflicting_form_values_are_rejected (test_state_tasks.SyntheticStateTaskTests.test_explicit_conflicting_form_values_are_rejected) ... ok +test_explicit_failed_mutation_is_not_rescued_by_a_plausible_delta (test_state_tasks.SyntheticStateTaskTests.test_explicit_failed_mutation_is_not_rescued_by_a_plausible_delta) ... ok +test_explicit_success_after_url_can_confirm_the_review (test_state_tasks.SyntheticStateTaskTests.test_explicit_success_after_url_can_confirm_the_review) ... ok +test_explicit_wrong_target_controls_are_rejected (test_state_tasks.SyntheticStateTaskTests.test_explicit_wrong_target_controls_are_rejected) ... ok +test_foreign_login_and_mutation_urls_do_not_count (test_state_tasks.SyntheticStateTaskTests.test_foreign_login_and_mutation_urls_do_not_count) ... ok +test_get_and_head_logout_405_observations_preserve_login (test_state_tasks.SyntheticStateTaskTests.test_get_and_head_logout_405_observations_preserve_login) ... ok +test_last_corrected_form_value_is_used (test_state_tasks.SyntheticStateTaskTests.test_last_corrected_form_value_is_used) ... ok +test_native_index_click_with_unknown_success_uses_later_page_and_delta (test_state_tasks.SyntheticStateTaskTests.test_native_index_click_with_unknown_success_uses_later_page_and_delta) ... ok +test_native_unknown_login_and_enter_submission_are_supported (test_state_tasks.SyntheticStateTaskTests.test_native_unknown_login_and_enter_submission_are_supported) ... ok +test_native_unknown_logout_needs_a_later_local_observation (test_state_tasks.SyntheticStateTaskTests.test_native_unknown_logout_needs_a_later_local_observation) ... ok +test_noop_is_not_success_even_if_answer_and_steps_claim_success (test_state_tasks.SyntheticStateTaskTests.test_noop_is_not_success_even_if_answer_and_steps_claim_success) ... ok +test_obsolete_logout_get_link_does_not_clear_login (test_state_tasks.SyntheticStateTaskTests.test_obsolete_logout_get_link_does_not_clear_login) ... ok +test_personal_and_review_ratings_are_distinct_from_imdb_context (test_state_tasks.SyntheticStateTaskTests.test_personal_and_review_ratings_are_distinct_from_imdb_context) ... ok +test_post_logout_button_or_enter_clears_the_previous_login (test_state_tasks.SyntheticStateTaskTests.test_post_logout_button_or_enter_clears_the_previous_login) ... ok +test_private_confirmation_cannot_belong_to_a_different_account (test_state_tasks.SyntheticStateTaskTests.test_private_confirmation_cannot_belong_to_a_different_account) ... ok +test_pure_navigation_does_not_prove_a_mutation_action (test_state_tasks.SyntheticStateTaskTests.test_pure_navigation_does_not_prove_a_mutation_action) ... ok +test_required_candidate_and_confirmation_pages_cannot_be_omitted (test_state_tasks.SyntheticStateTaskTests.test_required_candidate_and_confirmation_pages_cannot_be_omitted) ... ok +test_review_confirmation_can_be_concise_but_cannot_claim_a_wrong_headline (test_state_tasks.SyntheticStateTaskTests.test_review_confirmation_can_be_concise_but_cannot_claim_a_wrong_headline) ... ok +test_task15_either_qualifying_genre_is_sufficient (test_state_tasks.SyntheticStateTaskTests.test_task15_either_qualifying_genre_is_sufficient) ... ok +test_task15_removes_earliest_qualifying_tv_with_alphabetical_tie_break (test_state_tasks.SyntheticStateTaskTests.test_task15_removes_earliest_qualifying_tv_with_alphabetical_tie_break) ... ok +test_task15_removing_an_additional_watchlist_item_is_rejected (test_state_tasks.SyntheticStateTaskTests.test_task15_removing_an_additional_watchlist_item_is_rejected) ... ok +test_task15_reports_both_qualifying_genres_when_both_present (test_state_tasks.SyntheticStateTaskTests.test_task15_reports_both_qualifying_genres_when_both_present) ... ok +test_task15_target_title_removal_and_confirmation_are_valid (test_state_tasks.SyntheticStateTaskTests.test_task15_target_title_removal_and_confirmation_are_valid) ... ok +test_task15_wrong_tie_choice_type_or_genre_is_rejected (test_state_tasks.SyntheticStateTaskTests.test_task15_wrong_tie_choice_type_or_genre_is_rejected) ... ok +test_task16_another_users_rating_or_global_rating_cannot_change (test_state_tasks.SyntheticStateTaskTests.test_task16_another_users_rating_or_global_rating_cannot_change) ... ok +test_task16_existing_rating_keeps_id_and_created_at (test_state_tasks.SyntheticStateTaskTests.test_task16_existing_rating_keeps_id_and_created_at) ... ok +test_task16_newer_but_already_eight_and_nonmovie_or_nongenre_are_excluded (test_state_tasks.SyntheticStateTaskTests.test_task16_newer_but_already_eight_and_nonmovie_or_nongenre_are_excluded) ... ok +test_task16_null_personal_rating_is_eligible (test_state_tasks.SyntheticStateTaskTests.test_task16_null_personal_rating_is_eligible) ... ok +test_task16_unrated_target_gets_one_new_rating (test_state_tasks.SyntheticStateTaskTests.test_task16_unrated_target_gets_one_new_rating) ... ok +test_task16_updates_only_existing_target_rating (test_state_tasks.SyntheticStateTaskTests.test_task16_updates_only_existing_target_rating) ... ok +test_task17_any_review_sort_and_direct_or_reviews_entry_are_valid (test_state_tasks.SyntheticStateTaskTests.test_task17_any_review_sort_and_direct_or_reviews_entry_are_valid) ... ok +test_task17_created_review_fields_must_match (test_state_tasks.SyntheticStateTaskTests.test_task17_created_review_fields_must_match) ... ok +test_task17_creates_exactly_one_review_without_personal_rating_write (test_state_tasks.SyntheticStateTaskTests.test_task17_creates_exactly_one_review_without_personal_rating_write) ... ok +test_task17_extra_review_or_rating_write_is_rejected (test_state_tasks.SyntheticStateTaskTests.test_task17_extra_review_or_rating_write_is_rejected) ... ok +test_task17_new_review_body_can_be_any_nonempty_text (test_state_tasks.SyntheticStateTaskTests.test_task17_new_review_body_can_be_any_nonempty_text) ... ok +test_unknown_after_url_alone_does_not_confirm_mutation (test_state_tasks.SyntheticStateTaskTests.test_unknown_after_url_alone_does_not_confirm_mutation) ... ok +test_watchlist_candidate_page_must_belong_to_the_requested_account (test_state_tasks.SyntheticStateTaskTests.test_watchlist_candidate_page_must_belong_to_the_requested_account) ... ok +test_every_task_entry_rejects_another_task_or_missing_artifacts (test_verify_entry.VerifierEntryTests.test_every_task_entry_rejects_another_task_or_missing_artifacts) ... ok +test_missing_after_snapshot_never_falls_back_to_live_database (test_verify_entry.VerifierEntryTests.test_missing_after_snapshot_never_falls_back_to_live_database) ... ok +test_only_absolute_run_dir_argument_works_from_another_cwd (test_verify_entry.VerifierEntryTests.test_only_absolute_run_dir_argument_works_from_another_cwd) ... ok +test_same_id_old_question_is_not_current_candidate_evidence (test_verify_entry.VerifierEntryTests.test_same_id_old_question_is_not_current_candidate_evidence) ... ok +test_cli_arguments (test_verify_lib.SyntheticRunTests.test_cli_arguments) ... ok +test_connections_are_readonly_rows_and_closed (test_verify_lib.SyntheticRunTests.test_connections_are_readonly_rows_and_closed) ... ok +test_default_http_port_and_hostname_case_are_equivalent (test_verify_lib.SyntheticRunTests.test_default_http_port_and_hostname_case_are_equivalent) ... ok +test_emit_result_is_one_json_object_and_exit_status (test_verify_lib.SyntheticRunTests.test_emit_result_is_one_json_object_and_exit_status) ... ok +test_empty_business_table_added_removed_or_schema_changed (test_verify_lib.SyntheticRunTests.test_empty_business_table_added_removed_or_schema_changed) ... ok +test_failed_after_url_does_not_prove_navigation (test_verify_lib.SyntheticRunTests.test_failed_after_url_does_not_prove_navigation) ... ok +test_failure_overrides_other_success_flags_and_unknown_is_not_success (test_verify_lib.SyntheticRunTests.test_failure_overrides_other_success_flags_and_unknown_is_not_success) ... ok +test_foreign_events_same_path_and_embedded_path_do_not_count (test_verify_lib.SyntheticRunTests.test_foreign_events_same_path_and_embedded_path_do_not_count) ... ok +test_foreign_start_urls_rejected (test_verify_lib.SyntheticRunTests.test_foreign_start_urls_rejected) ... ok +test_initial_filename_and_explicit_overrides (test_verify_lib.SyntheticRunTests.test_initial_filename_and_explicit_overrides) ... ok +test_local_start_hosts_and_runtime_port_override (test_verify_lib.SyntheticRunTests.test_local_start_hosts_and_runtime_port_override) ... ok +test_malformed_success_status_does_not_crash_or_prove_navigation (test_verify_lib.SyntheticRunTests.test_malformed_success_status_does_not_crash_or_prove_navigation) ... ok +test_missing_snapshots_fail_without_live_fallback (test_verify_lib.SyntheticRunTests.test_missing_snapshots_fail_without_live_fallback) ... ok +test_multiset_change_and_extra_table_write_detected_without_secret_log (test_verify_lib.SyntheticRunTests.test_multiset_change_and_extra_table_write_detected_without_secret_log) ... ok +test_ordered_visits_require_each_event_but_no_implicit_home (test_verify_lib.SyntheticRunTests.test_ordered_visits_require_each_event_but_no_implicit_home) ... ok +test_question_whitespace_only_normalization_and_final_answer (test_verify_lib.SyntheticRunTests.test_question_whitespace_only_normalization_and_final_answer) ... ok +test_reordered_rows_are_unchanged (test_verify_lib.SyntheticRunTests.test_reordered_rows_are_unchanged) ... ok +test_sqlite_internal_tables_are_ignored (test_verify_lib.SyntheticRunTests.test_sqlite_internal_tables_are_ignored) ... ok +test_start_url_alone_is_not_visit_evidence (test_verify_lib.SyntheticRunTests.test_start_url_alone_is_not_visit_evidence) ... ok +test_storage_types_and_null_are_not_stringified (test_verify_lib.SyntheticRunTests.test_storage_types_and_null_are_not_stringified) ... ok +test_tail_slash_query_subsets_and_decoding (test_verify_lib.SyntheticRunTests.test_tail_slash_query_subsets_and_decoding) ... ok +test_wrong_task_replay_and_question_replay_rejected (test_verify_lib.SyntheticRunTests.test_wrong_task_replay_and_question_replay_rejected) ... ok + +---------------------------------------------------------------------- +Ran 146 tests in 13.018s + +OK diff --git a/review-reports/assets/pr33-imdb/evidence.json b/review-reports/assets/pr33-imdb/evidence.json new file mode 100644 index 00000000..dbe3662b --- /dev/null +++ b/review-reports/assets/pr33-imdb/evidence.json @@ -0,0 +1,101 @@ +{ + "source-home.jpg": { + "sha256": "1057284d277c8f3f3c4d0c6473f1ea068a2c840372d993a7c6b7b5265a5c7283", + "evidence_purpose": "source/before/after visual comparison", + "source_url": "https://www.imdb.com/", + "date": "2026-09-08", + "note": "Raw capture. Source/before IAB; candidate Chrome. Browser rendering is not calibrated for pixel-difference acceptance." + }, + "before-home.jpg": { + "sha256": "ff00c1bd8ca59ccdfa2c638747efe8216ccf2e33f80ef0d2b1e3f844629f5add", + "evidence_purpose": "source/before/after visual comparison", + "source_url": "local IMDb mirror", + "date": "2026-09-08", + "note": "Raw capture. Source/before IAB; candidate Chrome. Browser rendering is not calibrated for pixel-difference acceptance." + }, + "candidate-home.jpg": { + "sha256": "6256e87f96a14a2328f9005501f1c10b585ff25fff459c42836e18bc3c28ef0a", + "evidence_purpose": "source/before/after visual comparison", + "source_url": "local IMDb mirror", + "date": "2026-09-08", + "note": "Raw capture. Source/before IAB; candidate Chrome. Browser rendering is not calibrated for pixel-difference acceptance." + }, + "source-detail.jpg": { + "sha256": "5449248853c0f3b063f85b0a0661a7091f3809f5f60eeaa58bab5f7c04f3be3c", + "evidence_purpose": "source/before/after visual comparison", + "source_url": "https://www.imdb.com/title/tt0468569/", + "date": "2026-09-08", + "note": "Raw capture. Source/before IAB; candidate Chrome. Browser rendering is not calibrated for pixel-difference acceptance." + }, + "candidate-detail.jpg": { + "sha256": "fb389ac4f682df3c566ba73a0568a6898c5d4f88311f2380232b9b91f8c236d6", + "evidence_purpose": "source/before/after visual comparison", + "source_url": "local IMDb mirror", + "date": "2026-09-08", + "note": "Raw capture. Source/before IAB; candidate Chrome. Browser rendering is not calibrated for pixel-difference acceptance." + }, + "csrf-rejected.png": { + "sha256": "57c52acd9906c1497055d6d28cfa37d2a07e6fc78ca208a89451a8d0adf1ec40", + "evidence_purpose": "guided cross-port CSRF rejection", + "source_url": "local IMDb mirror", + "date": "2026-09-08", + "note": "Raw capture. Source/before IAB; candidate Chrome. Browser rendering is not calibrated for pixel-difference acceptance." + }, + "account-logout.png": { + "sha256": "f904115d9667f573d3651ce777d130d7beaa97360382af06cbd8a3c7ce3f96b8", + "evidence_purpose": "guided account-body logout regression", + "date": "2026-09-08", + "code_sha": "50bcce503637522d15bdafa533381204750f415f", + "asset_revision": "e70f49d8b6d0f32c940688e8e53e7c558f57a788" + }, + "mechanical-results.json": { + "sha256": "9252f1b8d6abd38280512494a39c1f52e6456faaf8b3a874d9bd7a3f40b77df6", + "evidence_purpose": "observed full22 mechanical checks on image74aec", + "date": "2026-09-08", + "code_sha": "50bcce503637522d15bdafa533381204750f415f", + "asset_revision": "e70f49d8b6d0f32c940688e8e53e7c558f57a788" + }, + "engineering-tests.txt": { + "sha256": "c470404f65876e9cd3c945994b3f85a92ddb59006e1cb5f38dcec1812afa0b47", + "evidence_purpose": "146 engineering tests on image74aec before parser-only fix", + "date": "2026-09-08", + "code_sha": "50bcce503637522d15bdafa533381204750f415f", + "asset_revision": "e70f49d8b6d0f32c940688e8e53e7c558f57a788" + }, + "round4-home-1440.jpg": { + "sha256": "e01ed704fac5781702dbee016db06cf3319afb2530cbac96495893780916f0e9", + "evidence_purpose": "current candidate homepage, top 1500px of the 1440x900-viewport full-page capture, downscaled to 1200px wide", + "source_url": "local IMDb mirror (immutable candidate image)", + "date": "2026-09-12", + "code_sha": "1c8a1eba19c084e202ecbab5977280701b9068c2", + "asset_revision": "f9ddfd2596229f2610418d57fc88c3051e1056bb", + "note": "Chromium capture on the fixed candidate; derived from the full-page PNG whose SHA256 is recorded in candidate-regression-1c8a1eb-summary.json. Owner accepted the expanded result after comparing with live IMDb; anonymous headless capture of the live source returned 403, so no new source screenshot is included." + }, + "round4-feature-1440.jpg": { + "sha256": "c38b3ef60c800db5cc145a3302b0db2f00fb7c112e6a489aeed1f6e0a2096251", + "evidence_purpose": "current candidate /feature/featured-today-1, top 1500px of the 1440-wide full-page capture, downscaled to 1200px wide", + "source_url": "local IMDb mirror (immutable candidate image)", + "date": "2026-09-12", + "code_sha": "1c8a1eba19c084e202ecbab5977280701b9068c2", + "asset_revision": "f9ddfd2596229f2610418d57fc88c3051e1056bb", + "note": "Chromium capture on the fixed candidate; derived from the full-page PNG whose SHA256 is recorded in candidate-regression-1c8a1eb-summary.json. Owner accepted the expanded result after comparing with live IMDb; anonymous headless capture of the live source returned 403, so no new source screenshot is included." + }, + "round4-home-390.jpg": { + "sha256": "f3cc74b1d73a13c979986664f38e6290dc1a3ecfd793d3275911569c25796823", + "evidence_purpose": "current candidate homepage at 390px width, top 1600px of the full-page capture", + "source_url": "local IMDb mirror (immutable candidate image)", + "date": "2026-09-12", + "code_sha": "1c8a1eba19c084e202ecbab5977280701b9068c2", + "asset_revision": "f9ddfd2596229f2610418d57fc88c3051e1056bb", + "note": "Chromium capture on the fixed candidate; derived from the full-page PNG whose SHA256 is recorded in candidate-regression-1c8a1eb-summary.json. Owner accepted the expanded result after comparing with live IMDb; anonymous headless capture of the live source returned 403, so no new source screenshot is included." + }, + "round4-feature-detail-1440.jpg": { + "sha256": "9b4321bb67dbafb94e6ec3f798d987f6fc7c74daf244daa5fe81b5807a879925", + "evidence_purpose": "current candidate feature detail route (first entry), top 900px at 1440 width, downscaled to 1200px wide", + "source_url": "local IMDb mirror (immutable candidate image)", + "date": "2026-09-12", + "code_sha": "1c8a1eba19c084e202ecbab5977280701b9068c2", + "asset_revision": "f9ddfd2596229f2610418d57fc88c3051e1056bb", + "note": "Chromium capture on the fixed candidate; derived from the full-page PNG whose SHA256 is recorded in candidate-regression-1c8a1eb-summary.json. Owner accepted the expanded result after comparing with live IMDb; anonymous headless capture of the live source returned 403, so no new source screenshot is included." + } +} diff --git a/review-reports/assets/pr33-imdb/formal-blind-review-1c8a1eb-summary.json b/review-reports/assets/pr33-imdb/formal-blind-review-1c8a1eb-summary.json new file mode 100644 index 00000000..047a10dc --- /dev/null +++ b/review-reports/assets/pr33-imdb/formal-blind-review-1c8a1eb-summary.json @@ -0,0 +1,197 @@ +{ + "schema": "imdb-public-formal-blind-review-summary/v1", + "completed_at": "2026-09-12T15:20:29Z", + "reviewer": { + "harness": "Claude Code CLI, fresh non-persistent session restricted to Read and read-only Bash; network tools disabled (web search/fetch requests: 0)", + "model": "claude-opus-5", + "model_provenance": "canonicalModel reported in the Claude CLI usage record of the same session (not only a self-declaration)", + "session_id": "89bbcff5-1861-4acc-a25a-50f4818223f6", + "num_turns": 79, + "permission_denials": 0 + }, + "target": { + "code_sha": "1c8a1eba19c084e202ecbab5977280701b9068c2", + "asset_revision": "f9ddfd2596229f2610418d57fc88c3051e1056bb", + "archive_sha256": "0663d30fe90ed2a0659dfe78ecf7ab1718970f565a64c6855a2c6aaadc606513", + "seed_sha256": "9d843c5388ecbc0d5265ac316cdaee915f8b203121b5a5330d207af5fb6206b0", + "task_file_sha256": "36214b6cf168c726d60e1ea0662ddfbfce6fa5dc77c804812460d1c38da545d8" + }, + "input": { + "manifest_sha256": "09875b57a8c3f9092eae5f6f00fb619715dbecf5eeefd8f698ac2c5c5dc343fa", + "files": 826, + "hash_mismatches": [], + "tasks": 20, + "source_packets": [ + { + "packet": "imdb-blind-review-24af75e31259", + "manifest_sha256": "60b1c9420a886fd028128dfe087cb375c587e5de2f0ea753e96fea2b0b705951", + "task_ids": [ + "IMDb--0", + "IMDb--2", + "IMDb--7", + "IMDb--9", + "IMDb--10", + "IMDb--12", + "IMDb--14", + "IMDb--15", + "IMDb--16", + "IMDb--17" + ] + }, + { + "packet": "imdb-expansion-blind-review-3d9fc7205c55", + "manifest_sha256": "f609c60f5229c0f91d75bbdea3dd2ff29ce8e82d6534cc9cfb7f0ac9183dbfdd", + "task_ids": [ + "IMDb--18", + "IMDb--19", + "IMDb--20", + "IMDb--21", + "IMDb--22", + "IMDb--23", + "IMDb--24", + "IMDb--25", + "IMDb--26", + "IMDb--27" + ] + } + ], + "excluded": [ + "deterministic verifier source and verdicts", + "hidden or expected answers", + "coordinator/Codex task verdicts and reconciliation conclusions", + "prior Claude reviews", + "repository contents and PR discussion" + ], + "review_scope": "Judge whether each frozen execution completes its supplied task. Do not certify current source fidelity, verifier correctness, or runner independence." + }, + "result": { + "packet_status": "VALID", + "pass": 20, + "fail": 0, + "structured_result_sha256": "6bbb06ae20ebb6a1441c4767e7c07ab4ba758f6a63c3ce1a21a3cdc5023c23c6", + "raw_response_sha256": "f9004bb9d63989cdb8e3c323dfb1ac813a1acf7dda0c96ab5a7ad2880c958164", + "hashed_before_verdicts_were_read": true + }, + "verdicts": [ + { + "task_id": "IMDb--0", + "verdict": "PASS", + "basis": "Chart opened from homepage; rank #1 and #3 both visited and both runtime/MPAA pairs correctly bound to their films, with the longer-runtime comparison correct. Read-only task and before.db/after.db are byte-identical." + }, + { + "task_id": "IMDb--2", + "verdict": "PASS", + "basis": "The run opened the catalog Domestic box office chart (explicitly ranked by cumulative US & Canada gross), identified its #1 title, and read that same title's Budget from its detail page. State unchanged." + }, + { + "task_id": "IMDb--7", + "verdict": "PASS", + "basis": "Correct person profile reached; the Director section (9 credits) was read as the candidate set, and ratings for every one of those credits were visible on the on-site search-results listing the run had already observed. The Dark Knight at 9.1 is the unique maximum." + }, + { + "task_id": "IMDb--9", + "verdict": "PASS", + "basis": "All four required Advanced-search controls were submitted and are visible as applied on the results page. The three reported titles retain both titles above the third-place 9.0 cutoff and pick one of the 9.0-tied titles, which the task explicitly permits." + }, + { + "task_id": "IMDb--10", + "verdict": "PASS", + "basis": "All constraints submitted with inclusive year bounds (1990 and 1999 results both present). Both 8.8-tied top results were opened and their directors and worldwide grosses read and correctly bound." + }, + { + "task_id": "IMDb--12", + "verdict": "PASS", + "basis": "Crime genre page was visited, and on-site Advanced-search type filters were used to separate the movie and TV-series groups (rubric permits this). Godfather 9.2 is the unique top Crime movie and Breaking Bad 9.5 the unique top Crime series; the 0.3 comparison is correct." + }, + { + "task_id": "IMDb--14", + "verdict": "PASS", + "basis": "Both named films were opened on the local mirror and each displayed rating and worldwide gross is correctly bound; both comparisons stated correctly with no false tie." + }, + { + "task_id": "IMDb--15", + "verdict": "PASS", + "basis": "Selection rule applied correctly against Bob's initial Watchlist: the only earlier-year entries (The Office 2005, Breaking Bad 2008) were checked and are not Sci-Fi/Fantasy, making Game of Thrones (2011, Fantasy) the earliest qualifying series. Exactly one watchlist row was deleted and absence was confirmed." + }, + { + "task_id": "IMDb--16", + "verdict": "PASS", + "basis": "Carol's initial Watchlist Crime movies were read; Fight Club (1999) is the most recently released and its initial personal rating was 9 (not 8), so it is the correct selection. Exactly one rating row was updated and confirmed on My ratings." + }, + { + "task_id": "IMDb--17", + "verdict": "PASS", + "basis": "Signed in as the specified account, posted exactly one review on Interstellar with the exact requested headline, review rating 10 and a nonempty body, then confirmed it on the movie's reviews page. No other business state changed." + }, + { + "task_id": "IMDb--18", + "verdict": "PASS", + "basis": "The complete 7-movie Animation/≤2010 candidate set was enumerated by on-site Advanced search with displayed ratings and runtimes; the run checked the three top-rated films' MPAA classifications, correctly excluded Grave of the Fireflies as Not Rated, and reported the unique optimum (16.9 sum, 186 min). No other pair can reach 16.9 given the displayed ratings, so the no-ties claim holds." + }, + { + "task_id": "IMDb--19", + "verdict": "PASS", + "basis": "Both credit lists were read from the correct profiles and the shared set is exactly the two reported films. Budget and Opening weekend US & Canada were read from each film's own detail page and both percentages round correctly to one decimal." + }, + { + "task_id": "IMDb--20", + "verdict": "PASS", + "basis": "All six movies in the mirror's DiCaprio Actor filmography are covered, each row's two displayed grosses were read from that film's own page, and every recomputed percentage matches the answer; the unique maximum is correctly identified." + }, + { + "task_id": "IMDb--21", + "verdict": "PASS", + "basis": "Each account's own ratings and Watchlist were read while signed in as that account, with no cross-account mixing; the per-account differences are exactly the four reported films, and each director and runtime is bound to the right title. No state changed." + }, + { + "task_id": "IMDb--22", + "verdict": "PASS", + "basis": "The deduplicated union of Alice's Watchlist and ratings is six movies, and the reviews page of each was inspected. Exactly one within-movie identical-headline group by different authors exists, reported with correct title/year/headline and author-bound score, date and helpful count." + }, + { + "task_id": "IMDb--23", + "verdict": "PASS", + "basis": "Every movie in the mirror's Tom Hanks Actor filmography was opened and its displayed Release date recorded; the chronological ordering and all consecutive gaps check out, and both 504-day pairs are reported." + }, + { + "task_id": "IMDb--24", + "verdict": "PASS", + "basis": "All specified filters were submitted with inclusive year bounds, David's initial Watchlist was read first, and exactly the two missing qualifying movies (runtime ≤120m) were added under his account; 121m Star Wars IV was correctly excluded. Existing entries preserved and additions confirmed on the Watchlist." + }, + { + "task_id": "IMDb--25", + "verdict": "PASS", + "basis": "Bob's ratings were read under his own login, Breaking Bad was confirmed a TV Series on its detail page and excluded, and exactly the one missing qualifying movie was added to David's Watchlist after a proper sign-out/sign-in. The already-saved recommendation is correctly identified and the destination list verified." + }, + { + "task_id": "IMDb--26", + "verdict": "PASS", + "basis": "The queue was derived correctly from Alice's initial Watchlist minus her personally rated titles, exactly one new account with the requested name and email was created, the two derived movies were saved to it, and a real sign-out followed by a successful re-login demonstrated persistence. Alice's data and all other rows are untouched." + }, + { + "task_id": "IMDb--27", + "verdict": "PASS", + "basis": "The two unrated Watchlist movies were distinguished and their reviews checked; only Interstellar has a scored review attributed to Alice (10/10), which was used as the rating source. Exactly one personal-rating row was inserted with that score, no review was created or altered, and My ratings was verified." + } + ], + "unexecuted_checks": [ + "Source fidelity was not certified: this pass did not verify that the frozen runs correspond to the current candidate code_sha 1c8a1eba19c084e202ecbab5977280701b9068c2 or asset_revision f9ddfd2596229f2610418d57fc88c3051e1056bb, nor that the mirror's catalog data matches upstream IMDb. Recorded executions carry different code_sha/asset_revision values (per execution-environment.json), and that divergence was neither investigated nor cleared.", + "Deterministic verifier correctness was not assessed: no verifier source, verdicts, expected answers, or hidden rubrics were read or executed, so these judgments are independent readings of task text plus run evidence and do not confirm or contradict any programmatic checker.", + "Runner independence was not evaluated: the claims in run-metadata.json (runner identity, 'no prior answer knowledge', guidance, human_interventions) were taken as unverified recorded metadata, and no check was made that the agent executed without answer leakage or outside assistance.", + "No repository files, Git history, PR discussion, coordinator conclusions, or prior reviews were consulted, and no network access was used; review inputs were limited to manifest.json and its 826 enumerated files.", + "Screenshot authenticity and pixel-level DOM/screenshot agreement were not exhaustively audited; screenshots were spot-checked (IMDb--17, IMDb--26) rather than reviewed for every step of every run.", + "Reproduction was not attempted: no run was re-executed against a live mirror, so timing, flakiness, and non-determinism in the frozen trajectories remain unexamined." + ], + "public_comment": "https://github.com/aiming-lab/WebHarbor/pull/89#issuecomment-5646790749", + "execution_versions_disclosed_to_reviewer": { + "IMDb--17": { + "code_sha": "ebe92f0888c5cf888fa40bd46f5b535ad01faa57", + "asset_revision": "4d5709e171d7c40fc742727adfa98b04b9023039" + }, + "all_other_tasks": { + "code_sha": "50bcce503637522d15bdafa533381204750f415f", + "asset_revision": "e70f49d8b6d0f32c940688e8e53e7c558f57a788" + } + }, + "limits": "Execution-completion judgment of frozen runs only; source fidelity, deterministic verifier correctness, runner independence and live replay were not assessed." +} diff --git a/review-reports/assets/pr33-imdb/independent-review-summary.json b/review-reports/assets/pr33-imdb/independent-review-summary.json new file mode 100644 index 00000000..de7ec27b --- /dev/null +++ b/review-reports/assets/pr33-imdb/independent-review-summary.json @@ -0,0 +1,112 @@ +{ + "status_update_2026-09-12": "Historical probe summary. Owner visual acceptance was recorded on 2026-09-12 and a formal 20-task blind review of the fixed candidate (formal-blind-review-1c8a1eb-summary.json) plus reconciliation (reconciliation-1c8a1eb-summary.json) supersede the pending statements below.", + "schema": "imdb-public-independent-review-summary/v1", + "reviewed_at": "2026-09-08T06:10:37Z", + "received_result_sha256": "adaba6206131a83cde4d583537bfa4377fad039df30179de374a7153b42b9244", + "input_manifest_sha256": "60b1c9420a886fd028128dfe087cb375c587e5de2f0ea753e96fea2b0b705951", + "target_candidate": "24af75e3125902a05a702b4e519a098b6d0d6588", + "reviewer": { + "harness": "Claude Code", + "model_declared": "claude-fable-5-1", + "identity_limit": "Identifier reported from the reviewer session system prompt; no independent runtime attestation." + }, + "coverage": { + "main_tasks": 10, + "pass": 10, + "fail": 0, + "guided_alternatives_reviewed": 0, + "packet_files_hash_checked": 288, + "screenshots_viewed": 10, + "other_screenshots_hash_checked": 98 + }, + "method": "Independent read-only review of frozen task/rubric, original trajectories, final answers, step DOM/screenshots and before/after SQLite snapshots. The reviewer declares no access to source, scoring results, hidden answer keys or prior review conclusions. Coordinator verified the received result hash and all packet files before reconciliation.", + "tasks": [ + { + "task_id": "IMDb--0", + "verdict": "PASS", + "execution_version": "r2", + "basis": "Chart ranks, title-bound runtimes and classifications, and the longer-runtime comparison are evidenced.", + "public_execution_excerpt": "tasks/task-results.json", + "final_screenshot": "tasks/IMDb--0-final.jpg" + }, + { + "task_id": "IMDb--2", + "verdict": "PASS", + "execution_version": "r2", + "basis": "The catalog domestic-gross leader is identified and its production budget is read from title details.", + "public_execution_excerpt": "tasks/task-results.json", + "final_screenshot": "tasks/IMDb--2-final.jpg" + }, + { + "task_id": "IMDb--7", + "verdict": "PASS", + "execution_version": "r2", + "basis": "The Director filmography defines the candidate set; on-site listing ratings establish the unique maximum.", + "public_execution_excerpt": "tasks/task-results.json", + "final_screenshot": "tasks/IMDb--7-final.jpg" + }, + { + "task_id": "IMDb--9", + "verdict": "PASS", + "execution_version": "r2", + "basis": "The requested filters are submitted; both above-cutoff titles plus one legal 9.0 cutoff tie are reported. A failed selector is recovered.", + "public_execution_excerpt": "tasks/task-results.json", + "final_screenshot": "tasks/IMDb--9-final.jpg" + }, + { + "task_id": "IMDb--10", + "verdict": "PASS", + "execution_version": "r2", + "basis": "All requested filters are applied and both tied leaders have correctly bound director and worldwide-gross facts.", + "public_execution_excerpt": "tasks/task-results.json", + "final_screenshot": "tasks/IMDb--10-final.jpg" + }, + { + "task_id": "IMDb--12", + "verdict": "PASS", + "execution_version": "r2", + "basis": "Crime movie and TV-series groups are inspected separately, with their leaders and ratings compared.", + "public_execution_excerpt": "tasks/task-results.json", + "final_screenshot": "tasks/IMDb--12-final.jpg" + }, + { + "task_id": "IMDb--14", + "verdict": "PASS", + "execution_version": "r2", + "basis": "Both title pages supply correctly bound ratings and worldwide grosses for the two comparisons.", + "public_execution_excerpt": "tasks/task-results.json", + "final_screenshot": "tasks/IMDb--14-final.jpg" + }, + { + "task_id": "IMDb--15", + "verdict": "PASS", + "execution_version": "r2", + "basis": "Initial watchlist and title facts establish the earliest qualifying series; only its watchlist row is removed and absence is confirmed.", + "public_execution_excerpt": "tasks/task-results.json", + "final_screenshot": "tasks/IMDb--15-final.jpg" + }, + { + "task_id": "IMDb--16", + "verdict": "PASS", + "execution_version": "r2", + "basis": "The latest watchlist movie qualifies by genre and initial personal rating; only its rating changes to 8 and My ratings confirms it.", + "public_execution_excerpt": "tasks/task-results.json", + "final_screenshot": "tasks/IMDb--16-final.jpg" + }, + { + "task_id": "IMDb--17", + "verdict": "PASS", + "execution_version": "r1", + "basis": "The specified account posts exactly one new review with the required headline, rating and nonempty body, then sees it in the title review list.", + "public_execution_excerpt": "tasks/task-results.json", + "final_screenshot": "tasks/IMDb--17-final.jpg" + } + ], + "limits": [ + "These verdicts apply to the recorded r1/r2 executions, not new UI executions at the target candidate head. Version reuse is assessed separately in the review report.", + "Seven read-only runs have byte-identical before/after databases; the three write runs contain only the requested single-row change.", + "Only ten key screenshots were viewed; the other 98 were hash-checked, not visually reviewed.", + "The original full raw result, databases and execution/handoff materials remain private; this file is a public summary, not the raw judgment artifact.", + "This review does not certify source fidelity, visual acceptance, verifier implementation or empirical model difficulty. Owner visual acceptance remains pending." + ] +} diff --git a/review-reports/assets/pr33-imdb/mechanical-results.json b/review-reports/assets/pr33-imdb/mechanical-results.json new file mode 100644 index 00000000..d9dda1b1 --- /dev/null +++ b/review-reports/assets/pr33-imdb/mechanical-results.json @@ -0,0 +1,784 @@ +{ + "started_at": "2026-09-08T02:00:28.513019+00:00", + "container": "wh-review033-candidate", + "purpose": "full22 mechanical HTTP/control/reset checks", + "sites": [ + "allrecipes", + "amazon", + "apple", + "arxiv", + "bbc_news", + "booking", + "github", + "google_flights", + "google_map", + "google_search", + "huggingface", + "wolfram_alpha", + "cambridge_dictionary", + "coursera", + "espn", + "merriam_webster", + "ikea", + "phys_org", + "target", + "ted", + "osu", + "imdb" + ], + "checks": { + "health_before": { + "url": "http://127.0.0.1:8961/health", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.024, + "body": { + "ok": true, + "sites": { + "allrecipes": { + "alive": true, + "pid": 53, + "port": 40000 + }, + "amazon": { + "alive": true, + "pid": 54, + "port": 40001 + }, + "apple": { + "alive": true, + "pid": 55, + "port": 40002 + }, + "arxiv": { + "alive": true, + "pid": 56, + "port": 40003 + }, + "bbc_news": { + "alive": true, + "pid": 57, + "port": 40004 + }, + "booking": { + "alive": true, + "pid": 58, + "port": 40005 + }, + "cambridge_dictionary": { + "alive": true, + "pid": 65, + "port": 40012 + }, + "coursera": { + "alive": true, + "pid": 66, + "port": 40013 + }, + "espn": { + "alive": true, + "pid": 67, + "port": 40014 + }, + "github": { + "alive": true, + "pid": 59, + "port": 40006 + }, + "google_flights": { + "alive": true, + "pid": 60, + "port": 40007 + }, + "google_map": { + "alive": true, + "pid": 61, + "port": 40008 + }, + "google_search": { + "alive": true, + "pid": 62, + "port": 40009 + }, + "huggingface": { + "alive": true, + "pid": 63, + "port": 40010 + }, + "ikea": { + "alive": true, + "pid": 69, + "port": 40016 + }, + "imdb": { + "alive": true, + "pid": 74, + "port": 40021 + }, + "merriam_webster": { + "alive": true, + "pid": 68, + "port": 40015 + }, + "osu": { + "alive": true, + "pid": 73, + "port": 40020 + }, + "phys_org": { + "alive": true, + "pid": 70, + "port": 40017 + }, + "target": { + "alive": true, + "pid": 71, + "port": 40018 + }, + "ted": { + "alive": true, + "pid": 72, + "port": 40019 + }, + "wolfram_alpha": { + "alive": true, + "pid": 64, + "port": 40011 + } + } + } + }, + "http_before": [ + { + "site": "allrecipes", + "url": "http://127.0.0.1:49000/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.048, + "body": null + }, + { + "site": "amazon", + "url": "http://127.0.0.1:49001/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.038, + "body": null + }, + { + "site": "apple", + "url": "http://127.0.0.1:49002/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.022, + "body": null + }, + { + "site": "arxiv", + "url": "http://127.0.0.1:49003/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.048, + "body": null + }, + { + "site": "bbc_news", + "url": "http://127.0.0.1:49004/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.06, + "body": null + }, + { + "site": "booking", + "url": "http://127.0.0.1:49005/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.034, + "body": null + }, + { + "site": "github", + "url": "http://127.0.0.1:49006/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.031, + "body": null + }, + { + "site": "google_flights", + "url": "http://127.0.0.1:49007/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.126, + "body": null + }, + { + "site": "google_map", + "url": "http://127.0.0.1:49008/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.029, + "body": null + }, + { + "site": "google_search", + "url": "http://127.0.0.1:49009/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.018, + "body": null + }, + { + "site": "huggingface", + "url": "http://127.0.0.1:49010/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.044, + "body": null + }, + { + "site": "wolfram_alpha", + "url": "http://127.0.0.1:49011/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.029, + "body": null + }, + { + "site": "cambridge_dictionary", + "url": "http://127.0.0.1:49012/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.02, + "body": null + }, + { + "site": "coursera", + "url": "http://127.0.0.1:49013/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.04, + "body": null + }, + { + "site": "espn", + "url": "http://127.0.0.1:49014/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.051, + "body": null + }, + { + "site": "merriam_webster", + "url": "http://127.0.0.1:49015/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.021, + "body": null + }, + { + "site": "ikea", + "url": "http://127.0.0.1:49016/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.049, + "body": null + }, + { + "site": "phys_org", + "url": "http://127.0.0.1:49017/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.037, + "body": null + }, + { + "site": "target", + "url": "http://127.0.0.1:49018/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.049, + "body": null + }, + { + "site": "ted", + "url": "http://127.0.0.1:49019/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.021, + "body": null + }, + { + "site": "osu", + "url": "http://127.0.0.1:49020/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.022, + "body": null + }, + { + "site": "imdb", + "url": "http://127.0.0.1:49021/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.025, + "body": null + } + ], + "hashes_before": { + "seed": "69f849b9c61fb71349958fdbedc301162881e3a171eb65098c2e5f6623b82471", + "runtime": "69f849b9c61fb71349958fdbedc301162881e3a171eb65098c2e5f6623b82471" + }, + "dirty_method": "Create wh033_reset_probe SQLite table with one canary row; synthetic control-plane check only.", + "dirty_canary": { + "written": true + }, + "hashes_dirty": { + "seed": "69f849b9c61fb71349958fdbedc301162881e3a171eb65098c2e5f6623b82471", + "runtime": "648566b09a579aa0bda736dab13e98fa06fe5798d659974285be1cfd46529376" + }, + "reset_imdb": { + "url": "http://127.0.0.1:8961/reset/imdb", + "method": "POST", + "status": 200, + "elapsed_seconds": 0.708, + "body": { + "pid": 272, + "ready": true, + "site": "imdb" + } + }, + "hashes_after_reset": { + "seed": "69f849b9c61fb71349958fdbedc301162881e3a171eb65098c2e5f6623b82471", + "runtime": "69f849b9c61fb71349958fdbedc301162881e3a171eb65098c2e5f6623b82471" + }, + "reset_all": { + "url": "http://127.0.0.1:8961/reset-all", + "method": "POST", + "status": 200, + "elapsed_seconds": 3.241, + "body": { + "ok": true, + "sites": { + "allrecipes": { + "pid": 310, + "ready": true, + "site": "allrecipes" + }, + "amazon": { + "pid": 308, + "ready": true, + "site": "amazon" + }, + "apple": { + "pid": 317, + "ready": true, + "site": "apple" + }, + "arxiv": { + "pid": 333, + "ready": true, + "site": "arxiv" + }, + "bbc_news": { + "pid": 326, + "ready": true, + "site": "bbc_news" + }, + "booking": { + "pid": 295, + "ready": true, + "site": "booking" + }, + "cambridge_dictionary": { + "pid": 337, + "ready": true, + "site": "cambridge_dictionary" + }, + "coursera": { + "pid": 325, + "ready": true, + "site": "coursera" + }, + "espn": { + "pid": 319, + "ready": true, + "site": "espn" + }, + "github": { + "pid": 318, + "ready": true, + "site": "github" + }, + "google_flights": { + "pid": 334, + "ready": true, + "site": "google_flights" + }, + "google_map": { + "pid": 320, + "ready": true, + "site": "google_map" + }, + "google_search": { + "pid": 316, + "ready": true, + "site": "google_search" + }, + "huggingface": { + "pid": 330, + "ready": true, + "site": "huggingface" + }, + "ikea": { + "pid": 331, + "ready": true, + "site": "ikea" + }, + "imdb": { + "pid": 335, + "ready": true, + "site": "imdb" + }, + "merriam_webster": { + "pid": 332, + "ready": true, + "site": "merriam_webster" + }, + "osu": { + "pid": 327, + "ready": true, + "site": "osu" + }, + "phys_org": { + "pid": 328, + "ready": true, + "site": "phys_org" + }, + "target": { + "pid": 339, + "ready": true, + "site": "target" + }, + "ted": { + "pid": 329, + "ready": true, + "site": "ted" + }, + "wolfram_alpha": { + "pid": 324, + "ready": true, + "site": "wolfram_alpha" + } + } + } + }, + "health_after": { + "url": "http://127.0.0.1:8961/health", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.004, + "body": { + "ok": true, + "sites": { + "allrecipes": { + "alive": true, + "pid": 310, + "port": 40000 + }, + "amazon": { + "alive": true, + "pid": 308, + "port": 40001 + }, + "apple": { + "alive": true, + "pid": 317, + "port": 40002 + }, + "arxiv": { + "alive": true, + "pid": 333, + "port": 40003 + }, + "bbc_news": { + "alive": true, + "pid": 326, + "port": 40004 + }, + "booking": { + "alive": true, + "pid": 295, + "port": 40005 + }, + "cambridge_dictionary": { + "alive": true, + "pid": 337, + "port": 40012 + }, + "coursera": { + "alive": true, + "pid": 325, + "port": 40013 + }, + "espn": { + "alive": true, + "pid": 319, + "port": 40014 + }, + "github": { + "alive": true, + "pid": 318, + "port": 40006 + }, + "google_flights": { + "alive": true, + "pid": 334, + "port": 40007 + }, + "google_map": { + "alive": true, + "pid": 320, + "port": 40008 + }, + "google_search": { + "alive": true, + "pid": 316, + "port": 40009 + }, + "huggingface": { + "alive": true, + "pid": 330, + "port": 40010 + }, + "ikea": { + "alive": true, + "pid": 331, + "port": 40016 + }, + "imdb": { + "alive": true, + "pid": 335, + "port": 40021 + }, + "merriam_webster": { + "alive": true, + "pid": 332, + "port": 40015 + }, + "osu": { + "alive": true, + "pid": 327, + "port": 40020 + }, + "phys_org": { + "alive": true, + "pid": 328, + "port": 40017 + }, + "target": { + "alive": true, + "pid": 339, + "port": 40018 + }, + "ted": { + "alive": true, + "pid": 329, + "port": 40019 + }, + "wolfram_alpha": { + "alive": true, + "pid": 324, + "port": 40011 + } + } + } + }, + "http_after": [ + { + "site": "allrecipes", + "url": "http://127.0.0.1:49000/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.037, + "body": null + }, + { + "site": "amazon", + "url": "http://127.0.0.1:49001/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.023, + "body": null + }, + { + "site": "apple", + "url": "http://127.0.0.1:49002/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.013, + "body": null + }, + { + "site": "arxiv", + "url": "http://127.0.0.1:49003/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.014, + "body": null + }, + { + "site": "bbc_news", + "url": "http://127.0.0.1:49004/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.035, + "body": null + }, + { + "site": "booking", + "url": "http://127.0.0.1:49005/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.013, + "body": null + }, + { + "site": "github", + "url": "http://127.0.0.1:49006/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.016, + "body": null + }, + { + "site": "google_flights", + "url": "http://127.0.0.1:49007/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.063, + "body": null + }, + { + "site": "google_map", + "url": "http://127.0.0.1:49008/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.022, + "body": null + }, + { + "site": "google_search", + "url": "http://127.0.0.1:49009/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.009, + "body": null + }, + { + "site": "huggingface", + "url": "http://127.0.0.1:49010/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.02, + "body": null + }, + { + "site": "wolfram_alpha", + "url": "http://127.0.0.1:49011/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.019, + "body": null + }, + { + "site": "cambridge_dictionary", + "url": "http://127.0.0.1:49012/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.018, + "body": null + }, + { + "site": "coursera", + "url": "http://127.0.0.1:49013/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.029, + "body": null + }, + { + "site": "espn", + "url": "http://127.0.0.1:49014/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.034, + "body": null + }, + { + "site": "merriam_webster", + "url": "http://127.0.0.1:49015/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.011, + "body": null + }, + { + "site": "ikea", + "url": "http://127.0.0.1:49016/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.033, + "body": null + }, + { + "site": "phys_org", + "url": "http://127.0.0.1:49017/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.023, + "body": null + }, + { + "site": "target", + "url": "http://127.0.0.1:49018/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.029, + "body": null + }, + { + "site": "ted", + "url": "http://127.0.0.1:49019/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.014, + "body": null + }, + { + "site": "osu", + "url": "http://127.0.0.1:49020/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.012, + "body": null + }, + { + "site": "imdb", + "url": "http://127.0.0.1:49021/", + "method": "GET", + "status": 200, + "elapsed_seconds": 0.019, + "body": null + } + ], + "hashes_after_all": { + "seed": "69f849b9c61fb71349958fdbedc301162881e3a171eb65098c2e5f6623b82471", + "runtime": "69f849b9c61fb71349958fdbedc301162881e3a171eb65098c2e5f6623b82471" + } + }, + "complete": true, + "image": "sha256:74aec29b73596a5a11ba43a4a1d0b22b82a473b4d36fcb232c2235d83d4258e9", + "finished_at": "2026-09-08T02:00:33.611053+00:00" +} diff --git a/review-reports/assets/pr33-imdb/reconciliation-1c8a1eb-summary.json b/review-reports/assets/pr33-imdb/reconciliation-1c8a1eb-summary.json new file mode 100644 index 00000000..ad337278 --- /dev/null +++ b/review-reports/assets/pr33-imdb/reconciliation-1c8a1eb-summary.json @@ -0,0 +1,213 @@ +{ + "schema": "imdb-public-reconciliation-summary/v1", + "reconciled_at": "2026-09-12T15:40:00Z", + "target": { + "code_sha": "1c8a1eba19c084e202ecbab5977280701b9068c2", + "asset_revision": "f9ddfd2596229f2610418d57fc88c3051e1056bb" + }, + "inputs": { + "blind_review_result_sha256": "6bbb06ae20ebb6a1441c4767e7c07ab4ba758f6a63c3ce1a21a3cdc5023c23c6", + "blind_review_manifest_sha256": "09875b57a8c3f9092eae5f6f00fb619715dbecf5eeefd8f698ac2c5c5dc343fa" + }, + "tasks_compared": 20, + "verdict_differences": 0, + "substantive_findings": 0, + "comparison": [ + { + "task_id": "IMDb--0", + "execution_version": "r2 (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--2", + "execution_version": "r2 (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--7", + "execution_version": "r2 (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--9", + "execution_version": "r2 (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--10", + "execution_version": "r2 (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--12", + "execution_version": "r2 (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--14", + "execution_version": "r2 (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--15", + "execution_version": "r2 (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--16", + "execution_version": "r2 (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--17", + "execution_version": "r1 (ebe92f0 / HF 4d5709e)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--18", + "execution_version": "r2 expansion (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--19", + "execution_version": "r2 expansion (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--20", + "execution_version": "r2 expansion (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--21", + "execution_version": "r2 expansion (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--22", + "execution_version": "r2 expansion (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--23", + "execution_version": "r2 expansion (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--24", + "execution_version": "r2 expansion (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--25", + "execution_version": "r2 expansion (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--26", + "execution_version": "r2 expansion (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + }, + { + "task_id": "IMDb--27", + "execution_version": "r2 expansion (50bcce5 / HF e70f49d)", + "deterministic_verifier_on_canonical_run": "PASS", + "deterministic_verifier_on_guided_replay_at_1c8a1eb": "PASS", + "independent_blind_review_2026_09_12": "PASS", + "difference": null + } + ], + "common_pass_spot_checks": { + "method": "Read-only inspection of the frozen run packet after the blind result was hashed: task text, final answer, step DOM snapshots, run metadata and independently recomputed full-table logical row diffs of before/after SQLite snapshots. Coordinator reconciliation, not a second blind review.", + "IMDb--9": "Filters Movie/Drama/rating>=8.5/sort by rating applied; 42 results; answer keeps both above-cutoff titles plus one 9.0 tie; before/after DB identical.", + "IMDb--15": "Only watchlist_items row (user 2, Game of Thrones) removed; detail shows TV Series 2011 with Fantasy genre; final Watchlist excludes it.", + "IMDb--16": "Only user_ratings row (user 3, Fight Club) changed 9 -> 8; Fight Club 1999 is the latest Crime title in Carol's Watchlist; My ratings shows 8/10.", + "IMDb--17": "Exactly one review row added (Interstellar, user 1, rating 10, exact headline, non-empty body); pre-existing Alice review unchanged; reviews page shows the new review. r1 seed differs from current only on two unrelated title years.", + "IMDb--19": "Intersection is Catch Me If You Can and Saving Private Ryan; displayed Budget/Opening pairs $52.0M/$30.1M and $70.0M/$30.6M; displayed-value percentages 57.9% and 43.7%; raw stored values give 57.8% and 43.7% with the same maximum; DB unchanged.", + "IMDb--26": "Exactly one user (weekend.viewer@test.com) and two watchlist rows for that user added; no ratings/reviews; real Sign Out (\"Signed out.\") then successful re-login (\"Welcome back, Weekend Viewer.\") and Watchlist shows Interstellar and Parasite." + }, + "rulings": { + "IMDb--19": "Graded from the amounts displayed by the mirror as the task text requires: 30.1M / 52.0M = 57.9%. Hidden raw values would yield 57.8%; the unique maximum is unchanged either way.", + "IMDb--26": "Completion PASS is supported by same-run actions, DOM and the exact database delta. The run's run-start and seal-time metadata disagree (model label, prior-knowledge flag, runner identity wording, human-intervention list, tab id, resumed-session note), so this run is not represented as a reliably attested independent exploration." + }, + "version_reuse": { + "tasks_and_verifiers": "sites/imdb/tasks.jsonl (SHA256 36214b6cf168c726d60e1ea0662ddfbfce6fa5dc77c804812460d1c38da545d8) and the sites/imdb/verify tree are byte-identical between the expansion freeze 3d9fc7205c55e7403c8cc8637675649710820b48 and 1c8a1eba19c084e202ecbab5977280701b9068c2.", + "seed_data": "Logical (sorted-row) hashes of all ten business tables match between the r2 execution seed and the current seed; the current seed only adds the separate home_features table (219 rows). The r1 seed used by IMDb--17 differs only on two title-year corrections for titles that task never reads.", + "application": "Changes after the freeze add the sourced homepage, feature/collection/news/offline routes and a cookie-session recently-viewed list; search, advanced search, charts, genre, name, watchlist, rating, review, login, register and logout handlers are unchanged.", + "change_impact_check": "Guided replay of all 20 task paths on the immutable candidate image: 20/20 UI, 20/20 deterministic verifier, 311 steps, 13 read-only databases unchanged, 7 expected write deltas, 20/20 resets restoring the seed.", + "conclusion": "The 20 recorded canonical executions remain valid evidence for this candidate with their original r1/r2 identities disclosed; they are not relabeled as executions at 1c8a1eb, and the guided replay is not canonical credit or a new blind review." + }, + "historical_reviews": [ + { + "date": "2026-09-08", + "tasks": "0,2,7,9,10,12,14,15,16,17", + "result": "10 PASS / 0 FAIL", + "model": "claude-fable-5-1 (session-declared)", + "manifest_sha256": "60b1c9420a886fd028128dfe087cb375c587e5de2f0ea753e96fea2b0b705951" + }, + { + "date": "2026-09-10", + "tasks": "18-27", + "result": "10 PASS / 0 FAIL", + "model": "claude-opus-5 (session-declared)", + "manifest_sha256": "f609c60f5229c0f91d75bbdea3dd2ff29ce8e82d6534cc9cfb7f0ac9183dbfdd" + } + ] +} diff --git a/review-reports/assets/pr33-imdb/round4-feature-1440.jpg b/review-reports/assets/pr33-imdb/round4-feature-1440.jpg new file mode 100644 index 00000000..387786d7 Binary files /dev/null and b/review-reports/assets/pr33-imdb/round4-feature-1440.jpg differ diff --git a/review-reports/assets/pr33-imdb/round4-feature-detail-1440.jpg b/review-reports/assets/pr33-imdb/round4-feature-detail-1440.jpg new file mode 100644 index 00000000..8db69c4e Binary files /dev/null and b/review-reports/assets/pr33-imdb/round4-feature-detail-1440.jpg differ diff --git a/review-reports/assets/pr33-imdb/round4-home-1440.jpg b/review-reports/assets/pr33-imdb/round4-home-1440.jpg new file mode 100644 index 00000000..c3d661d7 Binary files /dev/null and b/review-reports/assets/pr33-imdb/round4-home-1440.jpg differ diff --git a/review-reports/assets/pr33-imdb/round4-home-390.jpg b/review-reports/assets/pr33-imdb/round4-home-390.jpg new file mode 100644 index 00000000..8b93078a Binary files /dev/null and b/review-reports/assets/pr33-imdb/round4-home-390.jpg differ diff --git a/review-reports/assets/pr33-imdb/scoring-results.json b/review-reports/assets/pr33-imdb/scoring-results.json new file mode 100644 index 00000000..4131a46e --- /dev/null +++ b/review-reports/assets/pr33-imdb/scoring-results.json @@ -0,0 +1,1146 @@ +{ + "summary": { + "status": "PASS_FOR_BOUNDED_RETAINED_TASK_SCORING", + "source_commit": "a37df75a873842be0ed4510baab5dbff361ac9ad", + "task_contract_sha256": "36094843882d4c7539b60ab757e0a57c7168e7163528c614e4303e5ed9b53347", + "method": "Deterministic native verifier; offline reuse of sealed evidence", + "main_tasks": { + "total": 10, + "passed": 10, + "task_ids": [ + 0, + 2, + 7, + 9, + 10, + 12, + 14, + 15, + 16, + 17 + ] + }, + "guided_alternates": { + "total": 3, + "passed": 3, + "task_ids": [ + 9, + 15, + 17 + ], + "counted_separately": true + }, + "calibration": { + "trajectory_derived_synthetic": { + "total": 30, + "matched_expected": 30, + "positive": 8, + "negative": 22 + }, + "retained_historical_fixtures": { + "total": 7, + "matched_expected": 7 + }, + "additional_unit_fixture": { + "total": 1, + "matched_expected": 1 + } + }, + "retired_tasks": [ + 3, + 4, + 8 + ], + "retired_inputs_status": "Not applicable to the current contract; historical results preserved", + "original_main_guided_files_preserved": 529, + "original_evidence_overwritten": false, + "new_independent_browser_executions": 0, + "outstanding_confirmed_applicable_misjudgments": 0, + "limitations": [ + "Bounded calibration is not an overall accuracy estimate.", + "Offline replay does not claim new browser execution or human visual acceptance." + ] + }, + "engineering": { + "image": "sha256:a7ee01b145cc1bd90511114d260211a176eb5ab41002095d9ed69fb1fa0dc7f0", + "code_head": "a37df75a873842be0ed4510baab5dbff361ac9ad", + "started_at": "2026-09-08T03:13:11.660135+00:00", + "finished_at": "2026-09-08T03:13:28.792729+00:00", + "command": "docker run --rm --network none --entrypoint python3 IMAGE -m unittest discover -s /opt/WebSyn/imdb/tests -v", + "returncode": 0, + "tests": 174, + "seconds": 16.02, + "log_sha256": "1813a8fded8be7ea1587b56f553a31a6fcc4436034616a96d71a8b41e3b3ad13", + "inputs_sha256": "59e88260595f80f125e8b7325c946332caff67b4665ee4c51d3e954e70788363" + }, + "scorer_inputs_sha256": "22817f45317b99dd93986c3ec19b6b9d9ce029897b039b4acd6cdbe0500cf21d", + "source_files": { + "agent_demo/eval_judge.py": "81b9986dd5da177a84a97d9e0a81257c3f6eb717b983a6ef26f23d465f9d9589", + "agent_demo/pyproject.toml": "7ec36abea69ffce0ec73e550db9b8d0b9625334b2edaefa72de65846efe4a885", + "agent_demo/uv.lock": "e17ab741bac0d4b675592ea61e2912e1c216aa987e4f6845dd4f034f9cb531f8", + "sites/imdb/tasks.jsonl": "36094843882d4c7539b60ab757e0a57c7168e7163528c614e4303e5ed9b53347", + "sites/imdb/verify/answer_checks.py": "4c744efd67dbfcd36c1e130d16e6ea6941f9de5d64fe09a21cd41c1936ae7eaf", + "sites/imdb/verify/read_tasks.py": "6b5c13357aa23857ef958ca59d2ba2d27394ed1bc084065ef390ace3e17dca33", + "sites/imdb/verify/state_tasks.py": "4ac723d2fb7b759c2aba1cd8966374f1e68b56b2ba6cd36de39425df747b57fd", + "sites/imdb/verify/verify_0.py": "2a35c08657b19d408068d777c17fa19a4f2b7106c80ac5a811657b7cfce85454", + "sites/imdb/verify/verify_10.py": "44d54b62675fa34f4d3a413a8d7790387399d6982f8cf59bcc1e409cc27864c0", + "sites/imdb/verify/verify_12.py": "898e9a5bbd24b888d3b8760ddb5bc49811fca29f58262c9ad145d6b2987885c8", + "sites/imdb/verify/verify_14.py": "e8d61fe3c0d9f433e62bd8515a45e25896c8c503c2137ad8f4f8643a0568877e", + "sites/imdb/verify/verify_15.py": "f188f9e4af799c48d50282482f341435896b978a89aabef8aa4d1048d13d608e", + "sites/imdb/verify/verify_16.py": "538998bed388fe23341a893503be92b39bf1567e28a9ff9084e8b899284d2eec", + "sites/imdb/verify/verify_17.py": "edf84b4a67ed1ccf0f26d54dd325ed98ffe11350e11a7af1890645bacbf454cd", + "sites/imdb/verify/verify_2.py": "783cba7806ce2067d3fe5d5a04627427f92de87ee50fbe3fbd10cf33d17f5e6b", + "sites/imdb/verify/verify_7.py": "61b5758599cb04a2f2b1d9feff626d1e1c55fc87854dd809803471abe6ed82b6", + "sites/imdb/verify/verify_9.py": "980dbe56314831be9831cf102b52c0f13b088dff2c60c5c2e4873b084317de1e", + "sites/imdb/verify/verify_lib.py": "c5f7217f1f15eae0a04c9db0567524eea07f2520d1e75bb78ce1727bbaf2f742", + "sites/imdb/verify/verify_main.py": "b9fa9c7798f49c5d0d34cc8c0526bd94e49ca4d64cd95395e0e51f6004160b6b" + }, + "claude_review": "PASS_10_RETAINED_MAIN_RUNS", + "main_and_guided_results": [ + { + "sample_id": "genuine-task0", + "task": 0, + "classification": "genuine_frozen_run_offline_replay", + "expected_pass": true, + "expected_basis": "Original chart screenshot ranks 1/3 plus both title headers show142/R and152/PG-13; answer binds ranks and reports Dark Knight longer. Readonly before/after hashes match.", + "source_manifest_sha256": "00c6abd29f835347b297ed4043461f5c29aa0d0c77ff82dba83d6dc2eae8ceac", + "evaluated_at_utc": "2026-09-08T03:12:26.278098+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--0", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Movie chart and both title pages visited; runtime, MPAA and comparison match", + "All business tables are unchanged" + ] + } + }, + { + "sample_id": "genuine-task2", + "task": 2, + "classification": "genuine_frozen_run_offline_replay", + "expected_pass": true, + "expected_basis": "Original cumulative domestic chart path leads to Endgame; snapshot maximum domestic gross and title budget356000000 agree with original budget screenshot356.0M and answer. Readonly snapshots match.", + "source_manifest_sha256": "24172de46845e45f4b648c3a5829a8ccc820e50f3bc5f7ed71e5d24a5757be81", + "evaluated_at_utc": "2026-09-08T03:12:27.351865+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--2", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Catalog cumulative-domestic chart winner and title-page budget match", + "All business tables are unchanged" + ] + } + }, + { + "sample_id": "genuine-task7", + "task": 7, + "classification": "genuine_frozen_run_offline_replay", + "expected_pass": true, + "expected_basis": "Nolan Director9-film set inspected on profile and ratings read in matching search; DarkKnight2008 maximum9.1, no ties.", + "source_manifest_sha256": "1d76438601213b64f14fdb48ffafaa02cbb3ced1c7547ac977b5853a0161d0ef", + "evaluated_at_utc": "2026-09-08T03:12:30.263213+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--7", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Director filmography used; all highest-rated movie ties include title, year and rating", + "All business tables are unchanged" + ] + } + }, + { + "sample_id": "genuine-task9", + "task": 9, + "classification": "genuine_frozen_run_offline_replay", + "expected_pass": true, + "expected_basis": "Movie/Drama/min8.5/rating-sort URL and visible result; two required top titles plus legal12AngryMen cutoff9.0, correctly ordered. Numeric ratings are voluntary correct metadata.", + "source_manifest_sha256": "fcb4c98f9399d21029cd83d941552d6f2efe5bc03e92192ab796d7bfe6e47ce5", + "evaluated_at_utc": "2026-09-08T03:12:33.188549+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--9", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Specified Advanced search submitted; three titles satisfy rank order and cutoff-tie policy", + "All business tables are unchanged" + ] + } + }, + { + "sample_id": "genuine-task10", + "task": 10, + "classification": "genuine_frozen_run_offline_replay", + "expected_pass": true, + "expected_basis": "Movie/Crime/1990-1999/min8.5/rating-sort observed; both8.8 highest ties have correct director and gross from corresponding details.", + "source_manifest_sha256": "d121b83d67ac70191e770c18220e17a9d2bbd8ff9fa0368ecc07fcc2008aaa3a", + "evaluated_at_utc": "2026-09-08T03:12:36.670126+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--10", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "All Advanced search constraints observed; all highest-rating ties bind directors and worldwide gross", + "All business tables are unchanged" + ] + } + }, + { + "sample_id": "genuine-task12", + "task": 12, + "classification": "genuine_frozen_run_offline_replay", + "expected_pass": true, + "expected_basis": "Crime genre observed; legal advanced-search movie/TV subsets confirmtype and topvalues Godfather1972/9.2 vs BreakingBad2008/9.5; comparison0.3 correct.", + "source_manifest_sha256": "c43841e16035533b97bcadc019e84e68316b1fc893eb2b426fa26d36c9310bec", + "evaluated_at_utc": "2026-09-08T03:12:38.706770+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--12", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Crime genre visited; both type groups, complete top ties, years, ratings and group comparison match", + "All business tables are unchanged" + ] + } + }, + { + "sample_id": "genuine-task14", + "task": 14, + "classification": "genuine_frozen_run_offline_replay", + "expected_pass": true, + "expected_basis": "Original title fields in own frozen snapshot bind9.1/1.0B toDarkKnight and8.8/839.8M toInception; original answer compares both correctly and local navigation reaches both details. Original snapshots byte-equal. Root also independently reported these facts before scoring.", + "source_manifest_sha256": "310b61ee13ce68d2374961ea21c00b460cdb3df3b913d38396309b3aabf1751f", + "evaluated_at_utc": "2026-09-08T03:12:40.585162+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--14", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Both movie pages visited; ratings and worldwide grosses bind correctly and both comparisons match", + "All business tables are unchanged" + ] + } + }, + { + "sample_id": "genuine-task15", + "task": 15, + "classification": "genuine_frozen_run_offline_replay", + "expected_pass": true, + "expected_basis": "Exactly Bob user2 initial eligible earliestTVFantasy GoT1872011 removed watchlist6 only; answer identifies selection and subsequentWatchlist. Other tables/schema unchanged.", + "source_manifest_sha256": "9017c2ea6d65e8dddf57f7b12015cd294d468fd89b648034ecf8af7a6d15d5ca", + "evaluated_at_utc": "2026-09-08T03:12:42.503821+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--15", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Bob's initial Watchlist rule selects the deleted series; exactly that entry was removed", + "Local removal and subsequent confirmation observed; title, year and qualifying genres match" + ] + } + }, + { + "sample_id": "genuine-task16", + "task": 16, + "classification": "genuine_frozen_run_offline_replay", + "expected_pass": true, + "expected_basis": "Exactly Carol user3 FightClub1141999 personalrating row10 changed9to8, sameid/date; selectedbyinitialWatchlistCrimemovieyearandpriorrating!=8; MyRatingsconfirmed; allothers unchanged.", + "source_manifest_sha256": "3e6fafc3831abdade50369aee405d87e68c21580b33835564b2a28ffa331368d", + "evaluated_at_utc": "2026-09-08T03:12:44.419710+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--16", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Carol's initial eligible Watchlist selects the rated movie; only its personal rating changed to 8", + "Local rating action followed by My ratings observed; reported title and year match" + ] + } + }, + { + "sample_id": "genuine-task17", + "task": 17, + "classification": "genuine_frozen_run_offline_replay", + "expected_pass": true, + "expected_basis": "Previously frozen task17-calibration expected-verdict.json: original form/list screenshots plus exact Alice/Interstellar single-review delta establish PASS before parser repair.", + "source_manifest_sha256": "f001c604b1e52377bf70d16061b250e2160f9025750adf3156c9ad7bd26a3c0d", + "evaluated_at_utc": "2026-09-08T03:12:46.056993+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--17", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Exactly one new Alice review matches the required Interstellar headline, rating and nonempty body", + "Local review submission followed by that movie's reviews page observed; no other business table changed" + ] + } + }, + { + "sample_id": "guided-task9-alternate01", + "task": 9, + "classification": "guided_existing_alternate_run_offline_replay", + "expected_pass": true, + "expected_basis": "Movie/Drama/min8.5/rating-sort Advanced UI and result screenshot007 support Shawshank9.3/Godfather9.2/GodfatherPartII9.0. The third choice is an allowed9.0 cutoff tie, no mutation.", + "source_manifest_sha256": "a1b4e566679e38bb2a7a8edf2d30a6ca0ddd8e0f3efc9456de4100bce126dd58", + "evaluated_at_utc": "2026-09-08T03:12:47.803844+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--9", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Specified Advanced search submitted; three titles satisfy rank order and cutoff-tie policy", + "All business tables are unchanged" + ] + } + }, + { + "sample_id": "guided-task15-alternate01", + "task": 15, + "classification": "guided_existing_alternate_run_offline_replay", + "expected_pass": true, + "expected_basis": "Initial Bob qualifying series GoT2011 versus StrangerThings2016 selects GoT; exactly watchlist row6 deleted, no other table/schema change. Local Watchlist row Remove and final anonymous-to-Bob list confirmation screenshot012 match answer.", + "source_manifest_sha256": "b48b2d7a6aaf0d17a44a2ebf022739691b67a56566821e0bc618997e49fa280a", + "evaluated_at_utc": "2026-09-08T03:12:50.178557+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--15", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Bob's initial Watchlist rule selects the deleted series; exactly that entry was removed", + "Local removal and subsequent confirmation observed; title, year and qualifying genres match" + ] + } + }, + { + "sample_id": "guided-task17-alternate01", + "task": 17, + "classification": "guided_existing_alternate_run_offline_replay", + "expected_pass": true, + "expected_basis": "Exactly one new Alice1/Interstellar182 review, headline Brilliant sci-fi epic, rating10, nonemptybody,helpful0,is_seed0; no other table change. Post action then Most recent visible screenshot013 confirms new review first. Allowed recent-sort alternative.", + "source_manifest_sha256": "71404034fd6d99cc168f607dc851139186fc793d0a7b7d8d67516a7baefb1450", + "evaluated_at_utc": "2026-09-08T03:12:51.888869+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--17", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Exactly one new Alice review matches the required Interstellar headline, rating and nonempty body", + "Local review submission followed by that movie's reviews page observed; no other business table changed" + ] + } + } + ], + "constructed_fixtures": [ + { + "sample_id": "synthetic-task0-legal_multiline", + "task": 0, + "classification": "synthetic_from_run", + "expected_pass": true, + "expected_basis": "Same correctly bound ranks, runtime and MPAA in separate English lines; visiting order is unrestricted.", + "parent_manifest_sha256": "00c6abd29f835347b297ed4043461f5c29aa0d0c77ff82dba83d6dc2eae8ceac", + "mutation": "legal_multiline", + "evaluated_at_utc": "2026-09-08T03:12:26.951539+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--0", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Movie chart and both title pages visited; runtime, MPAA and comparison match", + "All business tables are unchanged" + ] + } + }, + { + "sample_id": "synthetic-task0-wrong_ranks", + "task": 0, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "The task requires rank1/rank3 to bind to the chart movies; reversing ranks contradicts the original chart and snapshot.", + "parent_manifest_sha256": "00c6abd29f835347b297ed4043461f5c29aa0d0c77ff82dba83d6dc2eae8ceac", + "mutation": "wrong_ranks", + "evaluated_at_utc": "2026-09-08T03:12:29.452869+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--0", + "pass": false, + "reason": "A movie is bound to the wrong chart rank", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task0-answer_only", + "task": 0, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "The task requires opening the chart and reading both title pages; an answer without steps is insufficient.", + "parent_manifest_sha256": "00c6abd29f835347b297ed4043461f5c29aa0d0c77ff82dba83d6dc2eae8ceac", + "mutation": "answer_only", + "evaluated_at_utc": "2026-09-08T03:12:32.458269+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--0", + "pass": false, + "reason": "The Top 250 Movies chart was not visited", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task0-wrong_task_replay", + "task": 0, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "The verifier must evaluate the requested task identity, not a replay labeled as another task.", + "parent_manifest_sha256": "00c6abd29f835347b297ed4043461f5c29aa0d0c77ff82dba83d6dc2eae8ceac", + "mutation": "wrong_task_replay", + "evaluated_at_utc": "2026-09-08T03:12:35.185455+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--0", + "pass": false, + "reason": "Trajectory task ID does not match the expected task ID", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task0-extra_business_write", + "task": 0, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "Read-only rubric forbids unrelated business-table changes.", + "parent_manifest_sha256": "00c6abd29f835347b297ed4043461f5c29aa0d0c77ff82dba83d6dc2eae8ceac", + "mutation": "extra_business_write", + "evaluated_at_utc": "2026-09-08T03:12:37.633477+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--0", + "pass": false, + "reason": "Unexpected database changes in tables: genres", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task2-legal_dollar_units", + "task": 2, + "classification": "synthetic_from_run", + "expected_pass": true, + "expected_basis": "356 million dollars is exactly the displayed356.0M budget; original chart/title evidence is preserved.", + "parent_manifest_sha256": "24172de46845e45f4b648c3a5829a8ccc820e50f3bc5f7ed71e5d24a5757be81", + "mutation": "legal_dollar_units", + "evaluated_at_utc": "2026-09-08T03:12:39.516554+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--2", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Catalog cumulative-domestic chart winner and title-page budget match", + "All business tables are unchanged" + ] + } + }, + { + "sample_id": "synthetic-task2-domestic_as_budget", + "task": 2, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "Cumulative domestic gross is not the requested production budget.", + "parent_manifest_sha256": "24172de46845e45f4b648c3a5829a8ccc820e50f3bc5f7ed71e5d24a5757be81", + "mutation": "domestic_as_budget", + "evaluated_at_utc": "2026-09-08T03:12:41.196802+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--2", + "pass": false, + "reason": "The chart winner's production budget is missing or incorrect", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task2-missing_chart", + "task": 2, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "The question explicitly requires the cumulative domestic chart, not title details alone.", + "parent_manifest_sha256": "24172de46845e45f4b648c3a5829a8ccc820e50f3bc5f7ed71e5d24a5757be81", + "mutation": "missing_chart", + "evaluated_at_utc": "2026-09-08T03:12:43.211794+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--2", + "pass": false, + "reason": "The catalog domestic box-office chart was not visited", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task2-foreign_same_paths", + "task": 2, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "Identical paths on a foreign host are not this run local-mirror navigation.", + "parent_manifest_sha256": "24172de46845e45f4b648c3a5829a8ccc820e50f3bc5f7ed71e5d24a5757be81", + "mutation": "foreign_same_paths", + "evaluated_at_utc": "2026-09-08T03:12:44.897672+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--2", + "pass": false, + "reason": "The catalog domestic box-office chart was not visited", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task2-empty_answer", + "task": 2, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "The question requires reporting the movie and budget.", + "parent_manifest_sha256": "24172de46845e45f4b648c3a5829a8ccc820e50f3bc5f7ed71e5d24a5757be81", + "mutation": "empty_answer", + "evaluated_at_utc": "2026-09-08T03:12:46.216725+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--2", + "pass": false, + "reason": "The run has no final answer", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task17-legal_concise", + "task": 17, + "classification": "synthetic_from_run", + "expected_pass": true, + "expected_basis": "Task/rubric permit concise confirmation without repeating fixed headline; original exact review delta and navigation remain.", + "parent_manifest_sha256": "f001c604b1e52377bf70d16061b250e2160f9025750adf3156c9ad7bd26a3c0d", + "mutation": "legal_concise", + "evaluated_at_utc": "2026-09-08T03:12:47.933089+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--17", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Exactly one new Alice review matches the required Interstellar headline, rating and nonempty body", + "Local review submission followed by that movie's reviews page observed; no other business table changed" + ] + } + }, + { + "sample_id": "synthetic-task17-wrong_headline", + "task": 17, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "An explicit wrong headline contradicts the exact requested title even with correct DB delta.", + "parent_manifest_sha256": "f001c604b1e52377bf70d16061b250e2160f9025750adf3156c9ad7bd26a3c0d", + "mutation": "wrong_headline", + "evaluated_at_utc": "2026-09-08T03:12:50.228589+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--17", + "pass": false, + "reason": "The answer reports a different review headline", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task17-quoted_extra_suffix", + "task": 17, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "An extra suffix inside the quoted headline is not metadata and is not the requested exact headline.", + "parent_manifest_sha256": "f001c604b1e52377bf70d16061b250e2160f9025750adf3156c9ad7bd26a3c0d", + "mutation": "quoted_extra_suffix", + "evaluated_at_utc": "2026-09-08T03:12:51.881481+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--17", + "pass": false, + "reason": "The answer reports a different review headline", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task17-contradictory_rating", + "task": 17, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "The answer cannot report review rating9 when the task requires10.", + "parent_manifest_sha256": "f001c604b1e52377bf70d16061b250e2160f9025750adf3156c9ad7bd26a3c0d", + "mutation": "contradictory_rating", + "evaluated_at_utc": "2026-09-08T03:12:53.282843+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--17", + "pass": false, + "reason": "The answer reports a different review rating", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task17-noop_database", + "task": 17, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "Submitting exactly one new review is required; an unchanged after snapshot cannot be rescued by UI or answer claims.", + "parent_manifest_sha256": "f001c604b1e52377bf70d16061b250e2160f9025750adf3156c9ad7bd26a3c0d", + "mutation": "noop_database", + "evaluated_at_utc": "2026-09-08T03:12:54.307150+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--17", + "pass": false, + "reason": "Expected exactly one business table to change: reviews", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task17-extra_personal_rating", + "task": 17, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "The rubric allows one new review only; a separate personal-rating mutation is extra business state.", + "parent_manifest_sha256": "f001c604b1e52377bf70d16061b250e2160f9025750adf3156c9ad7bd26a3c0d", + "mutation": "extra_personal_rating", + "evaluated_at_utc": "2026-09-08T03:12:55.429214+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--17", + "pass": false, + "reason": "Expected exactly one business table to change: reviews", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task12-legal_multiline", + "task": 12, + "classification": "synthetic_from_run", + "expected_pass": true, + "expected_basis": "Legal explicit movie/TV labels and the same correct cross-group conclusion.", + "parent_manifest_sha256": "c43841e16035533b97bcadc019e84e68316b1fc893eb2b426fa26d36c9310bec", + "evaluated_at_utc": "2026-09-08T03:12:56.386617+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--12", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Crime genre visited; both type groups, complete top ties, years, ratings and group comparison match", + "All business tables are unchanged" + ] + } + }, + { + "sample_id": "synthetic-task12-reversed_groups", + "task": 12, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "Movie/TV labels swapped while all title/year/rating values remain correct.", + "parent_manifest_sha256": "c43841e16035533b97bcadc019e84e68316b1fc893eb2b426fa26d36c9310bec", + "evaluated_at_utc": "2026-09-08T03:12:57.755508+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--12", + "pass": false, + "reason": "The highest-rated titles are not identified as movie versus TV series", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task12-extra_false_winner", + "task": 12, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "A lower-rated Crime movie is explicitly added to the highest group.", + "parent_manifest_sha256": "c43841e16035533b97bcadc019e84e68316b1fc893eb2b426fa26d36c9310bec", + "evaluated_at_utc": "2026-09-08T03:12:59.313275+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--12", + "pass": false, + "reason": "An extra title is incorrectly reported as a highest-rating winner", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task12-wrong_comparison_group", + "task": 12, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "Correct per-group facts cannot rescue an explicit wrong cross-group winner.", + "parent_manifest_sha256": "c43841e16035533b97bcadc019e84e68316b1fc893eb2b426fa26d36c9310bec", + "evaluated_at_utc": "2026-09-08T03:13:00.796005+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--12", + "pass": false, + "reason": "The movie-versus-series top-rating comparison is missing or incorrect", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task16-legal_from_to", + "task": 16, + "classification": "synthetic_from_run", + "expected_pass": true, + "expected_basis": "Correct previous9/new8, ordinary numeric update wording.", + "parent_manifest_sha256": "3e6fafc3831abdade50369aee405d87e68c21580b33835564b2a28ffa331368d", + "mutation": "legal_from_to", + "evaluated_at_utc": "2026-09-08T03:13:01.801239+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--16", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Carol's initial eligible Watchlist selects the rated movie; only its personal rating changed to 8", + "Local rating action followed by My ratings observed; reported title and year match" + ] + } + }, + { + "sample_id": "synthetic-task16-legal_was_now", + "task": 16, + "classification": "synthetic_from_run", + "expected_pass": true, + "expected_basis": "Explicit was9 is initial value; now8 is correct result.", + "parent_manifest_sha256": "3e6fafc3831abdade50369aee405d87e68c21580b33835564b2a28ffa331368d", + "mutation": "legal_was_now", + "evaluated_at_utc": "2026-09-08T03:13:02.894875+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--16", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Carol's initial eligible Watchlist selects the rated movie; only its personal rating changed to 8", + "Local rating action followed by My ratings observed; reported title and year match" + ] + } + }, + { + "sample_id": "synthetic-task16-legal_saved_was", + "task": 16, + "classification": "synthetic_from_run", + "expected_pass": true, + "expected_basis": "Explicit saved label binds was8 to after state.", + "parent_manifest_sha256": "3e6fafc3831abdade50369aee405d87e68c21580b33835564b2a28ffa331368d", + "mutation": "legal_saved_was", + "evaluated_at_utc": "2026-09-08T03:13:04.358842+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--16", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Carol's initial eligible Watchlist selects the rated movie; only its personal rating changed to 8", + "Local rating action followed by My ratings observed; reported title and year match" + ] + } + }, + { + "sample_id": "synthetic-task16-wrong_previous", + "task": 16, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "Claimed previous7 contradicts frozen initial9 despite correct result8.", + "parent_manifest_sha256": "3e6fafc3831abdade50369aee405d87e68c21580b33835564b2a28ffa331368d", + "mutation": "wrong_previous", + "evaluated_at_utc": "2026-09-08T03:13:06.172948+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--16", + "pass": false, + "reason": "The rated title, release year, previous rating or personal 8/10 result is missing or incorrect", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task16-wrong_current", + "task": 16, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "Explicit current9 contradicts requested and saved8.", + "parent_manifest_sha256": "3e6fafc3831abdade50369aee405d87e68c21580b33835564b2a28ffa331368d", + "mutation": "wrong_current", + "evaluated_at_utc": "2026-09-08T03:13:07.195543+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--16", + "pass": false, + "reason": "The rated title, release year, previous rating or personal 8/10 result is missing or incorrect", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task16-threshold_only", + "task": 16, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "Eligibility threshold8 and old9 alone do not report required saved8.", + "parent_manifest_sha256": "3e6fafc3831abdade50369aee405d87e68c21580b33835564b2a28ffa331368d", + "mutation": "threshold_only", + "evaluated_at_utc": "2026-09-08T03:13:08.159352+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--16", + "pass": false, + "reason": "The rated title, release year, previous rating or personal 8/10 result is missing or incorrect", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task16-wrong_saved_was", + "task": 16, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "Saved9 is an explicit wrong result, not the previous9.", + "parent_manifest_sha256": "3e6fafc3831abdade50369aee405d87e68c21580b33835564b2a28ffa331368d", + "mutation": "wrong_saved_was", + "evaluated_at_utc": "2026-09-08T03:13:09.333096+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--16", + "pass": false, + "reason": "The rated title, release year, previous rating or personal 8/10 result is missing or incorrect", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task16-extra_write", + "task": 16, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "Task16 permits only selected personal rating; extra unrelated table write is forbidden.", + "parent_manifest_sha256": "3e6fafc3831abdade50369aee405d87e68c21580b33835564b2a28ffa331368d", + "mutation": "extra_write", + "evaluated_at_utc": "2026-09-08T03:13:10.490626+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--16", + "pass": false, + "reason": "Expected exactly one business table to change: user_ratings", + "evidence": [] + } + }, + { + "sample_id": "synthetic-task16-from_cooccurrence_legal", + "task": 16, + "classification": "synthetic_from_run", + "expected_pass": true, + "expected_basis": "Correct rating transition remains bound to FightClub despite a genuine separate From title mention.", + "parent_manifest_sha256": "3e6fafc3831abdade50369aee405d87e68c21580b33835564b2a28ffa331368d", + "mutation": "final_answer only; genuine From mention plus rating from clause", + "evaluated_at_utc": "2026-09-08T03:13:11.651808+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--16", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Carol's initial eligible Watchlist selects the rated movie; only its personal rating changed to 8", + "Local rating action followed by My ratings observed; reported title and year match" + ] + } + }, + { + "sample_id": "synthetic-task16-from_cooccurrence_wrong_previous", + "task": 16, + "classification": "synthetic_from_run", + "expected_pass": false, + "expected_basis": "Wrong previous7 contradicts initial9 and cannot disappear at the preposition from when another From title is named.", + "parent_manifest_sha256": "3e6fafc3831abdade50369aee405d87e68c21580b33835564b2a28ffa331368d", + "mutation": "final_answer only; genuine From mention plus rating from clause", + "evaluated_at_utc": "2026-09-08T03:13:13.426739+00:00", + "expectation_met": true, + "final_contract_applicability": "APPLICABLE_UNCHANGED_QUESTION_AND_RUBRIC", + "actual": { + "task_id": "IMDb--16", + "pass": false, + "reason": "The rated title, release year, previous rating or personal 8/10 result is missing or incorrect", + "evidence": [] + } + } + ], + "historical_and_unit_results": [ + { + "sample_id": "task0_control", + "task": 0, + "classification": "synthetic_reconstructed_history", + "expected_pass": true, + "expected_basis": "Previously frozen historical calibration facts and unchanged task text; this retained synthetic fixture is reused byte-for-byte. The original historical DB was not retained, so no original DB byte-identity claim.", + "evaluated_at_utc": "2026-09-08T03:12:27.612165+00:00", + "expectation_met": true, + "actual": { + "task_id": "IMDb--0", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Movie chart and both title pages visited; runtime, MPAA and comparison match", + "All business tables are unchanged" + ] + } + }, + { + "sample_id": "task0_reversed_word_ranks", + "task": 0, + "classification": "synthetic_reconstructed_history", + "expected_pass": false, + "expected_basis": "Previously frozen historical calibration facts and unchanged task text; this retained synthetic fixture is reused byte-for-byte. The original historical DB was not retained, so no original DB byte-identity claim.", + "evaluated_at_utc": "2026-09-08T03:12:27.831133+00:00", + "expectation_met": true, + "actual": { + "task_id": "IMDb--0", + "pass": false, + "reason": "A movie is bound to the wrong chart rank", + "evidence": [] + } + }, + { + "sample_id": "task10_control", + "task": 10, + "classification": "synthetic_reconstructed_history", + "expected_pass": true, + "expected_basis": "Previously frozen historical calibration facts and unchanged task text; this retained synthetic fixture is reused byte-for-byte. The original historical DB was not retained, so no original DB byte-identity claim.", + "evaluated_at_utc": "2026-09-08T03:12:28.070987+00:00", + "expectation_met": true, + "actual": { + "task_id": "IMDb--10", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "All Advanced search constraints observed; all highest-rating ties bind directors and worldwide gross", + "All business tables are unchanged" + ] + } + }, + { + "sample_id": "task10_extra_false_winner", + "task": 10, + "classification": "synthetic_reconstructed_history", + "expected_pass": false, + "expected_basis": "Previously frozen historical calibration facts and unchanged task text; this retained synthetic fixture is reused byte-for-byte. The original historical DB was not retained, so no original DB byte-identity claim.", + "evaluated_at_utc": "2026-09-08T03:12:28.312417+00:00", + "expectation_met": true, + "actual": { + "task_id": "IMDb--10", + "pass": false, + "reason": "A highest-rating results list includes a title outside the winning set", + "evidence": [] + } + }, + { + "sample_id": "task12_control", + "task": 12, + "classification": "synthetic_reconstructed_history", + "expected_pass": true, + "expected_basis": "Previously frozen historical calibration facts and unchanged task text; this retained synthetic fixture is reused byte-for-byte. The original historical DB was not retained, so no original DB byte-identity claim.", + "evaluated_at_utc": "2026-09-08T03:12:28.561229+00:00", + "expectation_met": true, + "actual": { + "task_id": "IMDb--12", + "pass": true, + "reason": "Task requirements satisfied by the supplied run", + "evidence": [ + "Crime genre visited; both type groups, complete top ties, years, ratings and group comparison match", + "All business tables are unchanged" + ] + } + }, + { + "sample_id": "task12_extra_false_winner", + "task": 12, + "classification": "synthetic_reconstructed_history", + "expected_pass": false, + "expected_basis": "Previously frozen historical calibration facts and unchanged task text; this retained synthetic fixture is reused byte-for-byte. The original historical DB was not retained, so no original DB byte-identity claim.", + "evaluated_at_utc": "2026-09-08T03:12:28.754658+00:00", + "expectation_met": true, + "actual": { + "task_id": "IMDb--12", + "pass": false, + "reason": "A highest-rating results list includes a title outside the winning set", + "evidence": [] + } + }, + { + "sample_id": "task14_swapped_entity_gross", + "task": 14, + "classification": "synthetic_reconstructed_history", + "expected_pass": false, + "expected_basis": "Previously frozen historical calibration facts and unchanged task text; this retained synthetic fixture is reused byte-for-byte. The original historical DB was not retained, so no original DB byte-identity claim.", + "evaluated_at_utc": "2026-09-08T03:12:28.924012+00:00", + "expectation_met": true, + "actual": { + "task_id": "IMDb--14", + "pass": false, + "reason": "A compared movie's worldwide gross is missing or incorrectly bound", + "evidence": [] + } + }, + { + "sample_id": "unit-task16-wrong_previous_bare", + "task": 16, + "classification": "synthetic_unit_fixture_not_browser_run", + "expected_pass": false, + "expected_basis": "Frozen synthetic initial personal rating6, after8; explicit previous9 is false under unchanged task16 contract.", + "evaluated_at_utc": "2026-09-08T03:12:29.133434+00:00", + "expectation_met": true, + "actual": { + "task_id": "IMDb--16", + "pass": false, + "reason": "The rated title, release year, previous rating or personal 8/10 result is missing or incorrect", + "evidence": [] + } + } + ], + "retired_inputs": [ + { + "sample_id": "genuine-task3", + "task": 3, + "classification": "genuine_frozen_run_offline_replay", + "status": "RETIRED_TASK", + "applicable": false + }, + { + "sample_id": "genuine-task4", + "task": 4, + "classification": "genuine_frozen_run_offline_replay", + "status": "RETIRED_TASK", + "applicable": false + }, + { + "sample_id": "genuine-task8", + "task": 8, + "classification": "genuine_frozen_run_offline_replay", + "status": "RETIRED_TASK", + "applicable": false + }, + { + "sample_id": "guided-task8-alternate01", + "task": 8, + "classification": "guided_existing_alternate_run_offline_replay", + "status": "RETIRED_TASK", + "applicable": false + }, + { + "sample_id": "synthetic-task3-legal_markdown_exact", + "task": 3, + "classification": "synthetic_from_run", + "status": "RETIRED_TASK", + "applicable": false + }, + { + "sample_id": "synthetic-task3-swapped_amounts", + "task": 3, + "classification": "synthetic_from_run", + "status": "RETIRED_TASK", + "applicable": false + }, + { + "sample_id": "synthetic-task3-failed_title_navigation", + "task": 3, + "classification": "synthetic_from_run", + "status": "RETIRED_TASK", + "applicable": false + }, + { + "sample_id": "synthetic-task3-missing_snapshot", + "task": 3, + "classification": "synthetic_from_run", + "status": "RETIRED_TASK", + "applicable": false + }, + { + "sample_id": "task4_control", + "task": 4, + "classification": "synthetic_reconstructed_history", + "status": "RETIRED_TASK", + "applicable": false + }, + { + "sample_id": "task4_extra_false_director", + "task": 4, + "classification": "synthetic_reconstructed_history", + "status": "RETIRED_TASK", + "applicable": false + }, + { + "sample_id": "task3_money_control", + "task": 3, + "classification": "synthetic_reconstructed_history", + "status": "RETIRED_TASK", + "applicable": false + }, + { + "sample_id": "task3_swapped_money_fields", + "task": 3, + "classification": "synthetic_reconstructed_history", + "status": "RETIRED_TASK", + "applicable": false + } + ], + "limits": [ + "Original runner context is disclosed in task excerpts; not fresh-context model trials.", + "Historical reconstructions are not claimed byte-identical to the original historical database, which was not retained.", + "The 30 constructed cases, 7 historical controls and 1 unit case are separate from browser runs." + ], + "independent_review_summary": "independent-review-summary.json" +} diff --git a/review-reports/assets/pr33-imdb/source-detail.jpg b/review-reports/assets/pr33-imdb/source-detail.jpg new file mode 100644 index 00000000..8943cfdd Binary files /dev/null and b/review-reports/assets/pr33-imdb/source-detail.jpg differ diff --git a/review-reports/assets/pr33-imdb/source-home.jpg b/review-reports/assets/pr33-imdb/source-home.jpg new file mode 100644 index 00000000..cc6061c0 Binary files /dev/null and b/review-reports/assets/pr33-imdb/source-home.jpg differ diff --git a/review-reports/assets/pr33-imdb/task-expansion-independent-review-summary.json b/review-reports/assets/pr33-imdb/task-expansion-independent-review-summary.json new file mode 100644 index 00000000..32e4c174 --- /dev/null +++ b/review-reports/assets/pr33-imdb/task-expansion-independent-review-summary.json @@ -0,0 +1,52 @@ +{ + "status_update_2026-09-12": "Historical probe summary. Owner visual acceptance was recorded on 2026-09-12 and a formal 20-task blind review of the fixed candidate (formal-blind-review-1c8a1eb-summary.json) plus reconciliation (reconciliation-1c8a1eb-summary.json) supersede the pending statements below.", + "schema": "imdb-public-expansion-independent-review-summary/v1", + "received_at": "2026-09-10T07:31:39Z", + "received_result_sha256": "badf06f34a2b5d830b87c0847f29d50a4951e3a2fc8b21e288a4bb7eedf96865", + "input_manifest_sha256": "f609c60f5229c0f91d75bbdea3dd2ff29ce8e82d6534cc9cfb7f0ac9183dbfdd", + "target_candidate": "3d9fc7205c55e7403c8cc8637675649710820b48", + "reviewer": { + "harness": "Claude Code", + "model_declared": "claude-opus-5 (Claude Opus 5)", + "identity_limit": "Model identifier declared by the reviewer session; no independent runtime attestation." + }, + "coverage": { + "task_ids": ["IMDb--18", "IMDb--19", "IMDb--20", "IMDb--21", "IMDb--22", "IMDb--23", "IMDb--24", "IMDb--25", "IMDb--26", "IMDb--27"], + "main_tasks": 10, + "pass": 10, + "fail": 0, + "packet_files_hash_checked": 540, + "recorded_steps": 219, + "selected_action_failures": 0, + "selected_capture_failures": 0, + "screenshots_spot_checked": ["IMDb--24 step 19", "IMDb--26 step 34", "IMDb--27 step 21"] + }, + "method": "Independent read-only review of frozen task/rubric, trajectories, event logs, final answers, step DOM, selected screenshots and read-only before/after SQLite snapshots. The reviewer replayed actions, checked facts against same-run DOM, recomputed calculations, enumerated complete candidate sets from the frozen before state, and compared every business table. Verifier source/results, expected labels, prior reviews, repository contents and network sources were excluded.", + "results": [ + {"task_id": "IMDb--18", "verdict": "PASS", "basis": "The constrained pair, unique optimum and read-only state are supported by the submitted filters, opened title details and complete frozen candidate set."}, + {"task_id": "IMDb--19", "verdict": "PASS", "basis": "Role-scoped filmographies, displayed monetary fields and the unique percentage maximum are correctly bound; displayed-value rounding is 57.9%."}, + {"task_id": "IMDb--20", "verdict": "PASS", "basis": "All six actor credits and their displayed domestic/worldwide grosses support the reported remainder shares and unique maximum."}, + {"task_id": "IMDb--21", "verdict": "PASS", "basis": "Separate account sessions, ratings and Watchlists support the complete movie-only set difference and detail facts without state changes."}, + {"task_id": "IMDb--22", "verdict": "PASS", "basis": "The deduplicated account-title union and all relevant review pages support exactly one complete duplicate-headline group."}, + {"task_id": "IMDb--23", "verdict": "PASS", "basis": "All six role-scoped movie credits were opened; full release dates support the ordered timeline and both tied minimum intervals."}, + {"task_id": "IMDb--24", "verdict": "PASS", "basis": "The full filter set yields exactly two qualifying titles, both added to the correct Watchlist while all prior rows remain."}, + {"task_id": "IMDb--25", "verdict": "PASS", "basis": "The source account's qualifying movie ratings and destination account's prior Watchlist support exactly one added recommendation."}, + {"task_id": "IMDb--26", "verdict": "PASS", "basis": "The run creates one requested account, persists the exact two-title queue through sign-out/sign-in, and changes no unrelated rows; producer provenance metadata is internally inconsistent."}, + {"task_id": "IMDb--27", "verdict": "PASS", "basis": "The account's Watchlist, ratings and review attribution identify one title, and exactly one personal-rating row is added at the review-derived score."} + ], + "state_delta": { + "read_only_tasks": "IMDb--18 through IMDb--23 changed no business-table rows.", + "IMDb--24": "+2 Watchlist rows for the requested existing user; no other change.", + "IMDb--25": "+1 Watchlist row for the requested destination user; no other change.", + "IMDb--26": "+1 user and +2 Watchlist rows for that new user; no original row changed and no rating/review was created.", + "IMDb--27": "+1 personal-rating row for the requested user/title; no other change." + }, + "limits": [ + "This review judges completion of the ten recorded runs; it does not certify source fidelity, verifier implementation, visual acceptance or empirical frontier-model difficulty.", + "Task 26 run-start and seal-time model, prior-answer-knowledge, runner-identity and intervention metadata disagree. The direct run evidence supports completion, but the producer metadata is not a reliable independent-exploration attestation.", + "Task 19 is computed from displayed rounded monetary values: 30.1 / 52.0 rounds to 57.9%. Hidden raw values would round to 57.8%, without changing the maximum.", + "The recorded run URL used port 40021 while the target registry uses port 40024; the task question/rubric and non-port runtime inputs are unchanged.", + "The target asset revision is a union commit; the reviewed IMDb archive SHA256 is unchanged from the recorded asset revision.", + "Owner visual acceptance remains pending, so formal gates remain sequential and the PR stays Draft." + ] +} diff --git a/review-reports/assets/pr33-imdb/task-expansion-summary.json b/review-reports/assets/pr33-imdb/task-expansion-summary.json new file mode 100644 index 00000000..dab639bd --- /dev/null +++ b/review-reports/assets/pr33-imdb/task-expansion-summary.json @@ -0,0 +1,49 @@ +{ + "schema": "imdb-public-task-expansion-summary/v1", + "observed_at": "2026-09-10T07:31:39Z", + "status": "SUPERSEDED_2026-09-12_SEE_FINAL_REPORT", + "candidate_tasks": 20, + "original_retained_ids": [0, 2, 7, 9, 10, 12, 14, 15, 16, 17], + "new_ids": [18, 19, 20, 21, 22, 23, 24, 25, 26, 27], + "task_family_counts": {"read_only": 13, "state_changing": 7}, + "new_runs": { + "canonical_complete": 10, + "deterministic_pass": 10, + "deterministic_fail": 0, + "recorded_steps": 219, + "selected_action_failures": 0, + "selected_capture_failures": 0, + "noncanonical_attempts_preserved": 2 + }, + "tests": { + "full_imdb_suite": {"passed": 289, "failed": 0}, + "focused_expansion_suite": {"passed": 115, "failed": 0} + }, + "contracts": { + "new_task_contract_sha256": "347db376e39a4a519ad70929ec9e05eadb69e37398d49f4296d9474f56e4a9de", + "candidate_tasks_jsonl_sha256": "7e602e00088a5be569be2468014ea1cc7bf2a8c88fe3296b85ae803d5f7b145a" + }, + "independent_review": { + "original_ten": "PASS_10_OF_10", + "new_ten": "PASS_10_OF_10", + "combined": "PASS_20_OF_20", + "expansion_input_manifest_sha256": "f609c60f5229c0f91d75bbdea3dd2ff29ce8e82d6534cc9cfb7f0ac9183dbfdd", + "expansion_result_sha256": "badf06f34a2b5d830b87c0847f29d50a4951e3a2fc8b21e288a4bb7eedf96865", + "expansion_packet_files": 540, + "summary": "task-expansion-independent-review-summary.json" + }, + "owner_visual_acceptance": "ACCEPTED_2026-09-12", + "limitations": [ + "Independent review judges recorded run completion; it does not certify source fidelity, verifier implementation or visual acceptance.", + "Task 26 run-start and seal-time provenance metadata conflict, so its completion evidence must not be described as a reliable independent-exploration attestation.", + "Task 19 uses the mirror-displayed amounts and rounds to 57.9%; hidden raw amounts would round to 57.8% without changing the maximum.", + "Recorded routes support a multi-step design judgment, not an empirical frontier-model difficulty claim.", + "Formal review gates remain sequential." + ], + "superseded_by": { + "formal_blind_review": "formal-blind-review-1c8a1eb-summary.json", + "reconciliation": "reconciliation-1c8a1eb-summary.json", + "candidate_regression": "candidate-regression-1c8a1eb-summary.json", + "note": "Counts and hashes above remain the 2026-09-10 observation; the fixed candidate advanced to code 1c8a1eba19c084e202ecbab5977280701b9068c2 / HF f9ddfd2596229f2610418d57fc88c3051e1056bb with task contracts unchanged." + } +} diff --git a/review-reports/assets/pr33-imdb/tasks/IMDb--0-final.jpg b/review-reports/assets/pr33-imdb/tasks/IMDb--0-final.jpg new file mode 100644 index 00000000..f618307b Binary files /dev/null and b/review-reports/assets/pr33-imdb/tasks/IMDb--0-final.jpg differ diff --git a/review-reports/assets/pr33-imdb/tasks/IMDb--10-final.jpg b/review-reports/assets/pr33-imdb/tasks/IMDb--10-final.jpg new file mode 100644 index 00000000..463e7726 Binary files /dev/null and b/review-reports/assets/pr33-imdb/tasks/IMDb--10-final.jpg differ diff --git a/review-reports/assets/pr33-imdb/tasks/IMDb--12-final.jpg b/review-reports/assets/pr33-imdb/tasks/IMDb--12-final.jpg new file mode 100644 index 00000000..86934e92 Binary files /dev/null and b/review-reports/assets/pr33-imdb/tasks/IMDb--12-final.jpg differ diff --git a/review-reports/assets/pr33-imdb/tasks/IMDb--14-final.jpg b/review-reports/assets/pr33-imdb/tasks/IMDb--14-final.jpg new file mode 100644 index 00000000..64ec3358 Binary files /dev/null and b/review-reports/assets/pr33-imdb/tasks/IMDb--14-final.jpg differ diff --git a/review-reports/assets/pr33-imdb/tasks/IMDb--15-final.jpg b/review-reports/assets/pr33-imdb/tasks/IMDb--15-final.jpg new file mode 100644 index 00000000..ae44df3e Binary files /dev/null and b/review-reports/assets/pr33-imdb/tasks/IMDb--15-final.jpg differ diff --git a/review-reports/assets/pr33-imdb/tasks/IMDb--16-final.jpg b/review-reports/assets/pr33-imdb/tasks/IMDb--16-final.jpg new file mode 100644 index 00000000..f5727852 Binary files /dev/null and b/review-reports/assets/pr33-imdb/tasks/IMDb--16-final.jpg differ diff --git a/review-reports/assets/pr33-imdb/tasks/IMDb--17-final.jpg b/review-reports/assets/pr33-imdb/tasks/IMDb--17-final.jpg new file mode 100644 index 00000000..402ddd76 Binary files /dev/null and b/review-reports/assets/pr33-imdb/tasks/IMDb--17-final.jpg differ diff --git a/review-reports/assets/pr33-imdb/tasks/IMDb--2-final.jpg b/review-reports/assets/pr33-imdb/tasks/IMDb--2-final.jpg new file mode 100644 index 00000000..2a6c910a Binary files /dev/null and b/review-reports/assets/pr33-imdb/tasks/IMDb--2-final.jpg differ diff --git a/review-reports/assets/pr33-imdb/tasks/IMDb--7-final.jpg b/review-reports/assets/pr33-imdb/tasks/IMDb--7-final.jpg new file mode 100644 index 00000000..3b3e6b1f Binary files /dev/null and b/review-reports/assets/pr33-imdb/tasks/IMDb--7-final.jpg differ diff --git a/review-reports/assets/pr33-imdb/tasks/IMDb--9-final.jpg b/review-reports/assets/pr33-imdb/tasks/IMDb--9-final.jpg new file mode 100644 index 00000000..8acc6d77 Binary files /dev/null and b/review-reports/assets/pr33-imdb/tasks/IMDb--9-final.jpg differ diff --git a/review-reports/assets/pr33-imdb/tasks/task-results.json b/review-reports/assets/pr33-imdb/tasks/task-results.json new file mode 100644 index 00000000..4038109c --- /dev/null +++ b/review-reports/assets/pr33-imdb/tasks/task-results.json @@ -0,0 +1,923 @@ +{ + "schema": "imdb-public-task-excerpts/v1", + "scope": "Ten retained task executions selected from thirteen sealed main runs. Retired tasks3/4/8 and all guided runs are excluded from this table.", + "evidence_limitations": [ + "One final screenshot per task is an excerpt, not evidence of the entire execution. Complete original events, step captures, DOM and database snapshots are retained privately.", + "Final answers are recorded runner claims. Both deterministic scoring and independent Claude review pass these ten frozen main executions; neither result certifies source fidelity or visual acceptance.", + "The retained task17 r1 pilot retains its actual executed version; it is not relabeled as r2 or as an execution at a later code head.", + "Only successful non-observe, non-scroll recorded actions are counted as substantive; failed attempts, bootstrap activity and preparation/bookkeeping operations are excluded.", + "The runner was isolated from implementation, hidden answer keys and scoring outputs, but retained its own preceding UI context, including retired tasks. These are not ten fresh-model-context trials and do not establish a model success rate." + ], + "task_quality_assessment": { + "at_least_3_tasks_with_5_actions": { + "status": "OBSERVED_PATH_THRESHOLD_MET", + "qualifying_task_ids": [ + "IMDb--9", + "IMDb--10", + "IMDb--12", + "IMDb--14", + "IMDb--15", + "IMDb--16", + "IMDb--17" + ], + "observed_count": 7, + "limitation": "These are successful actions on the recorded paths. They do not prove that every legal solution requires at least five actions; shorter legal routes were not exhaustively bounded." + }, + "at_least_2_multistep_reasoning_or_comparisons": { + "status": "SUPPORTED_BY_TASKS_AND_RECORDED_UI_PATHS", + "examples": [ + { + "task_id": "IMDb--0", + "evidence": "Two title information pages were opened; the final answer compares the two runtimes." + }, + { + "task_id": "IMDb--14", + "evidence": "Two title detail pages were opened; the final answer compares both ratings and worldwide grosses." + }, + { + "task_id": "IMDb--12", + "evidence": "Separate Crime movie and TV-series result sets were inspected; the final answer compares their top ratings." + } + ], + "limitation": "This supports the performed comparison/reasoning structure, not final grading correctness or uniform task difficulty." + }, + "frontier_model_challenge": { + "status": "NOT_VERIFIED", + "design_candidates": [ + "IMDb--10", + "IMDb--15", + "IMDb--16" + ], + "design_reason": "Multi-condition selection, tied leaders or account-specific state changes can introduce reasoning and precision demands.", + "limitation": "No empirical frontier difficulty conclusion is established here. A single recovered selector error is not evidence of task-level reasoning difficulty." + }, + "low_depth_observation": "Tasks3/4 were retired because the first title detail provided all requested values; task8 was retired for a stable common-knowledge shortcut. Original runs remain historical evidence." + }, + "tasks": [ + { + "task_id": "IMDb--0", + "source_run": "task-0/run-01", + "execution": { + "version": "r2", + "recorded_code_head": "50bcce503637522d15bdafa533381204750f415f", + "code_identity": "recorded code commit plus frozen image inputs", + "image": "sha256:74aec29b73596a5a11ba43a4a1d0b22b82a473b4d36fcb232c2235d83d4258e9", + "asset_revision": "e70f49d8b6d0f32c940688e8e53e7c558f57a788", + "asset_archive_sha256": "5199d6d2601d070e09b235a149855435f8bb121c768ee5ab36fd6118bf830b74", + "seed_sha256": "69f849b9c61fb71349958fdbedc301162881e3a171eb65098c2e5f6623b82471", + "source_inputs_manifest_sha256": "f8c80d0265456e664e15c174f15523b52b5dff0070dcf90365041946ebdee4eb", + "model": "GPT-6 (Codex; inherited runner model)", + "harness": "CUA + durable-recorder.mjs", + "harness_sha256": "9c4843ea68c52dd44acaff1efcc654183ba916428be764354c0ce937a5771e3b" + }, + "original_manifest_sha256": "00c6abd29f835347b297ed4043461f5c29aa0d0c77ff82dba83d6dc2eae8ceac", + "original_manifest_size": 8856, + "recorded_step_count": 5, + "successful_substantive_action_count": 4, + "successful_substantive_step_indices": [ + 0, + 1, + 2, + 3 + ], + "excluded_steps": { + "observe": [ + 4 + ], + "scroll": [], + "failed": [] + }, + "recorded_failures": [], + "before_db_sha256": "055663a1f0992fbf26da98b47e820707cbfad21bacd3f27fb996698c79b2e105", + "after_db_sha256": "055663a1f0992fbf26da98b47e820707cbfad21bacd3f27fb996698c79b2e105", + "ui_path_summary": "Home → Top 250 Movies → The Shawshank Redemption → chart → The Dark Knight.", + "visited_relative_routes": [ + "/", + "/chart/top", + "/title/tt0111161", + "/chart/top", + "/title/tt0468569" + ], + "final_answer": "#1: The Shawshank Redemption — 142 minutes, MPAA R. #3: The Dark Knight — 152 minutes, MPAA PG-13. The Dark Knight has the longer runtime, by 10 minutes.", + "representative_image": { + "path": "IMDb--0-final.jpg", + "sha256": "a0c108d08490c7c2b261a635c36c59b25be62143251a8f31eee8f7fdb299059e", + "size": 77141, + "original_jpeg_path": "screenshots/step_005.source.jpg", + "original_png_path": "screenshots/step_005.png", + "original_png_sha256": "dc117bde2ac98ee737d132ab159e60e9872902f2429a6589d0fbd1f606cee318", + "original_png_size": 363681, + "width": 1470, + "height": 836, + "trajectory_step": 4, + "image_role": "final recorded after-action capture", + "copy_method": "original JPEG bytes; no transcoding" + }, + "scorer_result": "PASS", + "claude_result": "PASS", + "runner_prior_ui_context": { + "prior_answer_knowledge": "No hidden answers, implementation, evaluator or prior review accessed; own preceding pilot UI runs IMDb--3 and IMDb--17 are known." + }, + "scorer_evidence": [ + "Movie chart and both title pages visited; runtime, MPAA and comparison match", + "All business tables are unchanged" + ], + "native_result_sha256": "567d06b82c4605d7911c7da21ffab4ba2d085a62efbc72c71c65f562afbe0907" + }, + { + "task_id": "IMDb--2", + "source_run": "task-2/run-01", + "execution": { + "version": "r2", + "recorded_code_head": "50bcce503637522d15bdafa533381204750f415f", + "code_identity": "recorded code commit plus frozen image inputs", + "image": "sha256:74aec29b73596a5a11ba43a4a1d0b22b82a473b4d36fcb232c2235d83d4258e9", + "asset_revision": "e70f49d8b6d0f32c940688e8e53e7c558f57a788", + "asset_archive_sha256": "5199d6d2601d070e09b235a149855435f8bb121c768ee5ab36fd6118bf830b74", + "seed_sha256": "69f849b9c61fb71349958fdbedc301162881e3a171eb65098c2e5f6623b82471", + "source_inputs_manifest_sha256": "f8c80d0265456e664e15c174f15523b52b5dff0070dcf90365041946ebdee4eb", + "model": "GPT-6 (Codex; inherited runner model)", + "harness": "CUA + durable-recorder.mjs", + "harness_sha256": "9c4843ea68c52dd44acaff1efcc654183ba916428be764354c0ce937a5771e3b" + }, + "original_manifest_sha256": "24172de46845e45f4b648c3a5829a8ccc820e50f3bc5f7ed71e5d24a5757be81", + "original_manifest_size": 9684, + "recorded_step_count": 6, + "successful_substantive_action_count": 4, + "successful_substantive_step_indices": [ + 0, + 2, + 3, + 4 + ], + "excluded_steps": { + "observe": [ + 1, + 5 + ], + "scroll": [], + "failed": [] + }, + "recorded_failures": [ + { + "kind": "BOOKKEEPING", + "summary": "The runner recorded that manifest creation first targeted an already-existing task-0 manifest and was rejected with EEXIST; the helper was then called with explicit task-2 parameters. This disclosure was appended at sealing." + } + ], + "before_db_sha256": "055663a1f0992fbf26da98b47e820707cbfad21bacd3f27fb996698c79b2e105", + "after_db_sha256": "055663a1f0992fbf26da98b47e820707cbfad21bacd3f27fb996698c79b2e105", + "ui_path_summary": "Home/menu → Domestic box office → Avengers: Endgame → title details.", + "visited_relative_routes": [ + "/", + "/chart/boxoffice", + "/title/tt4154796", + "/title/tt4154796#title-details" + ], + "final_answer": "The first-ranked movie on Domestic box office is Avengers: Endgame (2019). Its displayed production budget is $356.0M.", + "representative_image": { + "path": "IMDb--2-final.jpg", + "sha256": "85747a6e090c616889b333d12bfb8c05db76f1d5bf8e5d806ccc27ed5326034a", + "size": 39573, + "original_jpeg_path": "screenshots/step_006.source.jpg", + "original_png_path": "screenshots/step_006.png", + "original_png_sha256": "b53a82c31194c09949d084b9beba810f19f20ecb3cbe0d70b506aa47cb0b44ca", + "original_png_size": 110324, + "width": 1470, + "height": 780, + "trajectory_step": 5, + "image_role": "final recorded after-action capture", + "copy_method": "original JPEG bytes; no transcoding" + }, + "scorer_result": "PASS", + "claude_result": "PASS", + "runner_prior_ui_context": { + "prior_answer_knowledge": "No hidden answers, implementation, evaluator or prior review accessed; own preceding pilot UI runs IMDb--3 and IMDb--17 are known.", + "prior_own_ui_runs": [ + "IMDb--3", + "IMDb--17", + "IMDb--0" + ] + }, + "scorer_evidence": [ + "Catalog cumulative-domestic chart winner and title-page budget match", + "All business tables are unchanged" + ], + "native_result_sha256": "2d41bf497e69db20cfe0b8794fbef0eab6b0d943342716cfe6d2c6450d34c2bf" + }, + { + "task_id": "IMDb--7", + "source_run": "task-7/run-01", + "execution": { + "version": "r2", + "recorded_code_head": "50bcce503637522d15bdafa533381204750f415f", + "code_identity": "recorded code commit plus frozen image inputs", + "image": "sha256:74aec29b73596a5a11ba43a4a1d0b22b82a473b4d36fcb232c2235d83d4258e9", + "asset_revision": "e70f49d8b6d0f32c940688e8e53e7c558f57a788", + "asset_archive_sha256": "5199d6d2601d070e09b235a149855435f8bb121c768ee5ab36fd6118bf830b74", + "seed_sha256": "69f849b9c61fb71349958fdbedc301162881e3a171eb65098c2e5f6623b82471", + "source_inputs_manifest_sha256": "f8c80d0265456e664e15c174f15523b52b5dff0070dcf90365041946ebdee4eb", + "model": "GPT-6 (Codex; inherited runner model)", + "harness": "CUA + durable-recorder.mjs", + "harness_sha256": "9c4843ea68c52dd44acaff1efcc654183ba916428be764354c0ce937a5771e3b" + }, + "original_manifest_sha256": "1d76438601213b64f14fdb48ffafaa02cbb3ced1c7547ac977b5853a0161d0ef", + "original_manifest_size": 9315, + "recorded_step_count": 5, + "successful_substantive_action_count": 3, + "successful_substantive_step_indices": [ + 0, + 1, + 2 + ], + "excluded_steps": { + "observe": [ + 4 + ], + "scroll": [ + 3 + ], + "failed": [] + }, + "recorded_failures": [], + "before_db_sha256": "055663a1f0992fbf26da98b47e820707cbfad21bacd3f27fb996698c79b2e105", + "after_db_sha256": "055663a1f0992fbf26da98b47e820707cbfad21bacd3f27fb996698c79b2e105", + "ui_path_summary": "Home search → Christopher Nolan search results → Nolan profile and Director filmography.", + "visited_relative_routes": [ + "/", + "/search?s=all&q=Christopher+Nolan", + "/name/nm0634240" + ], + "final_answer": "Among movies in Christopher Nolan’s Director filmography on this mirror, The Dark Knight (2008) has the highest IMDb rating: 9.1/10. No other listed Director movie ties that rating.", + "representative_image": { + "path": "IMDb--7-final.jpg", + "sha256": "78beb4229bc00192e1aadaf9c42dcc78967e67a57411331298a1f970159cd8da", + "size": 140962, + "original_jpeg_path": "screenshots/step_005.source.jpg", + "original_png_path": "screenshots/step_005.png", + "original_png_sha256": "a27f66156ba45c74cf8f087459388b5f4a8dcf46d827e2b6f82a0d50a0dc6bb0", + "original_png_size": 345165, + "width": 1470, + "height": 780, + "trajectory_step": 4, + "image_role": "final recorded after-action capture", + "copy_method": "original JPEG bytes; no transcoding" + }, + "scorer_result": "PASS", + "claude_result": "PASS", + "runner_prior_ui_context": { + "prior_answer_knowledge": "No hidden answers, implementation, evaluator or prior review accessed; own preceding pilot UI runs IMDb--3 and IMDb--17 are known.", + "prior_own_ui_runs": [ + "IMDb--3", + "IMDb--17", + "IMDb--0", + "IMDb--2", + "IMDb--4" + ] + }, + "scorer_evidence": [ + "Director filmography used; all highest-rated movie ties include title, year and rating", + "All business tables are unchanged" + ], + "native_result_sha256": "786927e5c6e4ead9db90ebcacfc5a447827cb2e7532064e51d5b2e364ea03703" + }, + { + "task_id": "IMDb--9", + "source_run": "task-9/run-01", + "execution": { + "version": "r2", + "recorded_code_head": "50bcce503637522d15bdafa533381204750f415f", + "code_identity": "recorded code commit plus frozen image inputs", + "image": "sha256:74aec29b73596a5a11ba43a4a1d0b22b82a473b4d36fcb232c2235d83d4258e9", + "asset_revision": "e70f49d8b6d0f32c940688e8e53e7c558f57a788", + "asset_archive_sha256": "5199d6d2601d070e09b235a149855435f8bb121c768ee5ab36fd6118bf830b74", + "seed_sha256": "69f849b9c61fb71349958fdbedc301162881e3a171eb65098c2e5f6623b82471", + "source_inputs_manifest_sha256": "f8c80d0265456e664e15c174f15523b52b5dff0070dcf90365041946ebdee4eb", + "model": "GPT-6 (Codex; inherited runner model)", + "harness": "CUA + durable-recorder.mjs", + "harness_sha256": "9c4843ea68c52dd44acaff1efcc654183ba916428be764354c0ce937a5771e3b" + }, + "original_manifest_sha256": "fcb4c98f9399d21029cd83d941552d6f2efe5bc03e92192ab796d7bfe6e47ce5", + "original_manifest_size": 12921, + "recorded_step_count": 10, + "successful_substantive_action_count": 6, + "successful_substantive_step_indices": [ + 0, + 3, + 4, + 5, + 6, + 7 + ], + "excluded_steps": { + "observe": [ + 2, + 9 + ], + "scroll": [ + 8 + ], + "failed": [ + 1 + ] + }, + "recorded_failures": [ + { + "kind": "UI_ACTION", + "step": 1, + "action": "select_option", + "summary": "The Type label selector matched no element and timed out. A later select action at step 3 succeeded." + } + ], + "before_db_sha256": "055663a1f0992fbf26da98b47e820707cbfad21bacd3f27fb996698c79b2e105", + "after_db_sha256": "055663a1f0992fbf26da98b47e820707cbfad21bacd3f27fb996698c79b2e105", + "ui_path_summary": "Home → Advanced title search → filtered Drama movie results.", + "visited_relative_routes": [ + "/", + "/search/title", + "/search/title?title_type=movie&year_from=&year_to=&rating_min=8.5&genre=drama&sort=rating" + ], + "final_answer": "Three highest-rated matching titles, in descending rating order: The Shawshank Redemption — 9.3/10; The Godfather — 9.2/10; 12 Angry Men — 9.0/10.", + "representative_image": { + "path": "IMDb--9-final.jpg", + "sha256": "b3beac383d00e1e869ae2ccdc486117498c233fb1d2cd695848b72b54d1cd9c7", + "size": 107070, + "original_jpeg_path": "screenshots/step_010.source.jpg", + "original_png_path": "screenshots/step_010.png", + "original_png_sha256": "8232261d1f44f7fcaf0d5a2fd9f2693e22ccc8e8e0dd2449e446d099b6fc7bf1", + "original_png_size": 340598, + "width": 1470, + "height": 780, + "trajectory_step": 9, + "image_role": "final recorded after-action capture", + "copy_method": "original JPEG bytes; no transcoding" + }, + "scorer_result": "PASS", + "claude_result": "PASS", + "runner_prior_ui_context": { + "prior_answer_knowledge": "No hidden answers, implementation, evaluator or prior review accessed; own preceding pilot UI runs IMDb--3 and IMDb--17 are known.", + "prior_own_ui_runs": [ + "IMDb--3", + "IMDb--17", + "IMDb--0", + "IMDb--2", + "IMDb--4", + "IMDb--7", + "IMDb--8" + ] + }, + "scorer_evidence": [ + "Specified Advanced search submitted; three titles satisfy rank order and cutoff-tie policy", + "All business tables are unchanged" + ], + "native_result_sha256": "e75329d632a0997716bc42acc43f59e74e8c8f17606b5e72206fbb392350d922" + }, + { + "task_id": "IMDb--10", + "source_run": "task-10/run-01", + "execution": { + "version": "r2", + "recorded_code_head": "50bcce503637522d15bdafa533381204750f415f", + "code_identity": "recorded code commit plus frozen image inputs", + "image": "sha256:74aec29b73596a5a11ba43a4a1d0b22b82a473b4d36fcb232c2235d83d4258e9", + "asset_revision": "e70f49d8b6d0f32c940688e8e53e7c558f57a788", + "asset_archive_sha256": "5199d6d2601d070e09b235a149855435f8bb121c768ee5ab36fd6118bf830b74", + "seed_sha256": "69f849b9c61fb71349958fdbedc301162881e3a171eb65098c2e5f6623b82471", + "source_inputs_manifest_sha256": "f8c80d0265456e664e15c174f15523b52b5dff0070dcf90365041946ebdee4eb", + "model": "GPT-6 (Codex; inherited runner model)", + "harness": "CUA + durable-recorder.mjs", + "harness_sha256": "9c4843ea68c52dd44acaff1efcc654183ba916428be764354c0ce937a5771e3b" + }, + "original_manifest_sha256": "d121b83d67ac70191e770c18220e17a9d2bbd8ff9fa0368ecc07fcc2008aaa3a", + "original_manifest_size": 14205, + "recorded_step_count": 15, + "successful_substantive_action_count": 14, + "successful_substantive_step_indices": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13 + ], + "excluded_steps": { + "observe": [ + 14 + ], + "scroll": [], + "failed": [] + }, + "recorded_failures": [], + "before_db_sha256": "055663a1f0992fbf26da98b47e820707cbfad21bacd3f27fb996698c79b2e105", + "after_db_sha256": "055663a1f0992fbf26da98b47e820707cbfad21bacd3f27fb996698c79b2e105", + "ui_path_summary": "Home → Advanced title search → filtered 1990s Crime results → Fight Club details → results → Pulp Fiction details.", + "visited_relative_routes": [ + "/", + "/search/title", + "/search/title?title_type=movie&year_from=1990&year_to=1999&rating_min=8.5&genre=crime&sort=rating", + "/title/tt0137523", + "/title/tt0137523#title-details", + "/title/tt0137523", + "/search/title?title_type=movie&year_from=1990&year_to=1999&rating_min=8.5&genre=crime&sort=rating", + "/title/tt0110912", + "/title/tt0110912#title-details" + ], + "final_answer": "The highest-rated results are tied at 8.8/10: Fight Club (1999) — director David Fincher; displayed worldwide gross $102.4M. Pulp Fiction (1994) — director Quentin Tarantino; displayed worldwide gross $213.9M.", + "representative_image": { + "path": "IMDb--10-final.jpg", + "sha256": "17876a0661e1566310f0c4c0dde13f20eb899e1086cf422727ad6e50f73efb16", + "size": 53156, + "original_jpeg_path": "screenshots/step_015.source.jpg", + "original_png_path": "screenshots/step_015.png", + "original_png_sha256": "269bb3d14eba0394282f9346d4fa57deeface87502100d562ef215da2c0c180e", + "original_png_size": 133859, + "width": 1470, + "height": 780, + "trajectory_step": 14, + "image_role": "final recorded after-action capture", + "copy_method": "original JPEG bytes; no transcoding" + }, + "scorer_result": "PASS", + "claude_result": "PASS", + "runner_prior_ui_context": { + "prior_answer_knowledge": "No hidden answers, implementation, evaluator or prior review accessed; own preceding pilot UI runs IMDb--3 and IMDb--17 are known.", + "prior_own_ui_runs": [ + "IMDb--3", + "IMDb--17", + "IMDb--0", + "IMDb--2", + "IMDb--4", + "IMDb--7", + "IMDb--8", + "IMDb--9" + ] + }, + "scorer_evidence": [ + "All Advanced search constraints observed; all highest-rating ties bind directors and worldwide gross", + "All business tables are unchanged" + ], + "native_result_sha256": "bd7a5845eff9f0d37012a15bb1db4eb4e4e61b628d3335786eff0f8bc4ddf60a" + }, + { + "task_id": "IMDb--12", + "source_run": "task-12/run-01", + "execution": { + "version": "r2", + "recorded_code_head": "50bcce503637522d15bdafa533381204750f415f", + "code_identity": "recorded code commit plus frozen image inputs", + "image": "sha256:74aec29b73596a5a11ba43a4a1d0b22b82a473b4d36fcb232c2235d83d4258e9", + "asset_revision": "e70f49d8b6d0f32c940688e8e53e7c558f57a788", + "asset_archive_sha256": "5199d6d2601d070e09b235a149855435f8bb121c768ee5ab36fd6118bf830b74", + "seed_sha256": "69f849b9c61fb71349958fdbedc301162881e3a171eb65098c2e5f6623b82471", + "source_inputs_manifest_sha256": "f8c80d0265456e664e15c174f15523b52b5dff0070dcf90365041946ebdee4eb", + "model": "GPT-6 (Codex; inherited runner model)", + "harness": "CUA + durable-recorder.mjs", + "harness_sha256": "9c4843ea68c52dd44acaff1efcc654183ba916428be764354c0ce937a5771e3b" + }, + "original_manifest_sha256": "c43841e16035533b97bcadc019e84e68316b1fc893eb2b426fa26d36c9310bec", + "original_manifest_size": 12436, + "recorded_step_count": 11, + "successful_substantive_action_count": 9, + "successful_substantive_step_indices": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "excluded_steps": { + "observe": [ + 10 + ], + "scroll": [ + 9 + ], + "failed": [] + }, + "recorded_failures": [], + "before_db_sha256": "055663a1f0992fbf26da98b47e820707cbfad21bacd3f27fb996698c79b2e105", + "after_db_sha256": "055663a1f0992fbf26da98b47e820707cbfad21bacd3f27fb996698c79b2e105", + "ui_path_summary": "Home → The Godfather → Crime genre → Advanced title search → Crime movie results → Crime TV-series results.", + "visited_relative_routes": [ + "/", + "/title/tt0068646", + "/genre/crime", + "/search/title", + "/search/title?title_type=movie&year_from=&year_to=&rating_min=&genre=crime&sort=rating", + "/search/title?title_type=tvSeries&year_from=&year_to=&rating_min=&genre=crime&sort=rating" + ], + "final_answer": "Highest-rated Crime movie: The Godfather (1972), 9.2/10. Highest-rated Crime TV series: Breaking Bad (2008), 9.5/10. Neither group has a tie at its highest rating. The TV-series group has the higher top rating, by 0.3 points.", + "representative_image": { + "path": "IMDb--12-final.jpg", + "sha256": "7d725f7fac13510983aa18169804611d44d9afe73724f3ae2a18b4ba953f98a8", + "size": 100369, + "original_jpeg_path": "screenshots/step_011.source.jpg", + "original_png_path": "screenshots/step_011.png", + "original_png_sha256": "a54dbc5f00cb9554dc638ec843dc3bd6436997ea90f0a2d7d7432c22b6029c7e", + "original_png_size": 312178, + "width": 1470, + "height": 780, + "trajectory_step": 10, + "image_role": "final recorded after-action capture", + "copy_method": "original JPEG bytes; no transcoding" + }, + "scorer_result": "PASS", + "claude_result": "PASS", + "runner_prior_ui_context": { + "prior_answer_knowledge": "No hidden answers, implementation, evaluator or prior review accessed; own preceding pilot UI runs IMDb--3 and IMDb--17 are known.", + "prior_own_ui_runs": [ + "IMDb--3", + "IMDb--17", + "IMDb--0", + "IMDb--2", + "IMDb--4", + "IMDb--7", + "IMDb--8", + "IMDb--9", + "IMDb--10" + ] + }, + "scorer_evidence": [ + "Crime genre visited; both type groups, complete top ties, years, ratings and group comparison match", + "All business tables are unchanged" + ], + "native_result_sha256": "adc1bd71eda68563dae2daac083e4bf516a0f27272ab7a7f8797d945559c74c3" + }, + { + "task_id": "IMDb--14", + "source_run": "task-14/run-01", + "execution": { + "version": "r2", + "recorded_code_head": "50bcce503637522d15bdafa533381204750f415f", + "code_identity": "recorded code commit plus frozen image inputs", + "image": "sha256:74aec29b73596a5a11ba43a4a1d0b22b82a473b4d36fcb232c2235d83d4258e9", + "asset_revision": "e70f49d8b6d0f32c940688e8e53e7c558f57a788", + "asset_archive_sha256": "5199d6d2601d070e09b235a149855435f8bb121c768ee5ab36fd6118bf830b74", + "seed_sha256": "69f849b9c61fb71349958fdbedc301162881e3a171eb65098c2e5f6623b82471", + "source_inputs_manifest_sha256": "f8c80d0265456e664e15c174f15523b52b5dff0070dcf90365041946ebdee4eb", + "model": "GPT-6 (Codex; inherited runner model)", + "harness": "CUA + durable-recorder.mjs", + "harness_sha256": "9c4843ea68c52dd44acaff1efcc654183ba916428be764354c0ce937a5771e3b" + }, + "original_manifest_sha256": "310b61ee13ce68d2374961ea21c00b460cdb3df3b913d38396309b3aabf1751f", + "original_manifest_size": 10174, + "recorded_step_count": 7, + "successful_substantive_action_count": 6, + "successful_substantive_step_indices": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "excluded_steps": { + "observe": [ + 6 + ], + "scroll": [], + "failed": [] + }, + "recorded_failures": [], + "before_db_sha256": "055663a1f0992fbf26da98b47e820707cbfad21bacd3f27fb996698c79b2e105", + "after_db_sha256": "055663a1f0992fbf26da98b47e820707cbfad21bacd3f27fb996698c79b2e105", + "ui_path_summary": "Home → The Dark Knight details → Inception search → Inception details.", + "visited_relative_routes": [ + "/", + "/title/tt0468569", + "/title/tt0468569#title-details", + "/search?s=all&q=Inception", + "/title/tt1375666", + "/title/tt1375666#title-details" + ], + "final_answer": "The Dark Knight (2008): IMDb rating 9.1/10; displayed worldwide gross $1.0B. Inception (2010): IMDb rating 8.8/10; displayed worldwide gross $839.8M. The Dark Knight has both the higher rating and the larger worldwide gross; neither comparison is tied.", + "representative_image": { + "path": "IMDb--14-final.jpg", + "sha256": "1b0570d0246c9787b7b73538b4cdf3fb456afadba830b2581d128f258dcf7cef", + "size": 53914, + "original_jpeg_path": "screenshots/step_007.source.jpg", + "original_png_path": "screenshots/step_007.png", + "original_png_sha256": "3572395d0b104d25438d8796ef68edbc79522b6c66aebf1990185651f658d698", + "original_png_size": 136556, + "width": 1470, + "height": 780, + "trajectory_step": 6, + "image_role": "final recorded after-action capture", + "copy_method": "original JPEG bytes; no transcoding" + }, + "scorer_result": "PASS", + "claude_result": "PASS", + "runner_prior_ui_context": { + "prior_answer_knowledge": "No hidden answers, implementation, evaluator or prior review accessed; own preceding pilot UI runs IMDb--3 and IMDb--17 are known.", + "prior_own_ui_runs": [ + "IMDb--3", + "IMDb--17", + "IMDb--0", + "IMDb--2", + "IMDb--4", + "IMDb--7", + "IMDb--8", + "IMDb--9", + "IMDb--10", + "IMDb--12" + ] + }, + "scorer_evidence": [ + "Both movie pages visited; ratings and worldwide grosses bind correctly and both comparisons match", + "All business tables are unchanged" + ], + "native_result_sha256": "8a137f66480359994357bcf5f4dd26b9d27471f4ed1330d1c769a9965517ec30" + }, + { + "task_id": "IMDb--15", + "source_run": "task-15/run-01", + "execution": { + "version": "r2", + "recorded_code_head": "50bcce503637522d15bdafa533381204750f415f", + "code_identity": "recorded code commit plus frozen image inputs", + "image": "sha256:74aec29b73596a5a11ba43a4a1d0b22b82a473b4d36fcb232c2235d83d4258e9", + "asset_revision": "e70f49d8b6d0f32c940688e8e53e7c558f57a788", + "asset_archive_sha256": "5199d6d2601d070e09b235a149855435f8bb121c768ee5ab36fd6118bf830b74", + "seed_sha256": "69f849b9c61fb71349958fdbedc301162881e3a171eb65098c2e5f6623b82471", + "source_inputs_manifest_sha256": "f8c80d0265456e664e15c174f15523b52b5dff0070dcf90365041946ebdee4eb", + "model": "GPT-6 (Codex; inherited runner model)", + "harness": "CUA + durable-recorder.mjs", + "harness_sha256": "9c4843ea68c52dd44acaff1efcc654183ba916428be764354c0ce937a5771e3b" + }, + "original_manifest_sha256": "9017c2ea6d65e8dddf57f7b12015cd294d468fd89b648034ecf8af7a6d15d5ca", + "original_manifest_size": 14022, + "recorded_step_count": 14, + "successful_substantive_action_count": 12, + "successful_substantive_step_indices": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 11, + 12 + ], + "excluded_steps": { + "observe": [ + 10, + 13 + ], + "scroll": [], + "failed": [] + }, + "recorded_failures": [], + "before_db_sha256": "055663a1f0992fbf26da98b47e820707cbfad21bacd3f27fb996698c79b2e105", + "after_db_sha256": "7e46417c886d4ee6ba63bcb7bfbc898863cdbabacac3e186b092c0e534e33404", + "ui_path_summary": "Sign in → Bob’s Watchlist → candidate title details → Watchlist removal → remaining Watchlist.", + "visited_relative_routes": [ + "/", + "/login", + "/", + "/list/watchlist", + "/title/tt0386676", + "/list/watchlist", + "/title/tt0903747", + "/list/watchlist", + "/title/tt0944947", + "/list/watchlist" + ], + "final_answer": "Removed Game of Thrones (2011), a TV series with the qualifying genre Fantasy. It was the earliest qualifying series in Bob’s initial Watchlist. Confirmed it is no longer in Bob Chen’s Watchlist; Breaking Bad, Stranger Things, and The Office remain.", + "representative_image": { + "path": "IMDb--15-final.jpg", + "sha256": "d19d0ab97dcc7b2489b5b9035812bf7aa3ffadc12ae091b479fbb825a64e746c", + "size": 45337, + "original_jpeg_path": "screenshots/step_014.source.jpg", + "original_png_path": "screenshots/step_014.png", + "original_png_sha256": "43c3db57b58d37f97f3f1d26969e5bee5d819ca0305bb6f2c409ee5970bf4ff5", + "original_png_size": 129013, + "width": 1470, + "height": 780, + "trajectory_step": 13, + "image_role": "final recorded after-action capture", + "copy_method": "original JPEG bytes; no transcoding" + }, + "scorer_result": "PASS", + "claude_result": "PASS", + "runner_prior_ui_context": { + "prior_answer_knowledge": "No hidden answers, implementation, evaluator or prior review accessed; own preceding pilot UI runs IMDb--3 and IMDb--17 are known.", + "prior_own_ui_runs": [ + "IMDb--3", + "IMDb--17", + "IMDb--0", + "IMDb--2", + "IMDb--4", + "IMDb--7", + "IMDb--8", + "IMDb--9", + "IMDb--10", + "IMDb--12", + "IMDb--14" + ] + }, + "scorer_evidence": [ + "Bob's initial Watchlist rule selects the deleted series; exactly that entry was removed", + "Local removal and subsequent confirmation observed; title, year and qualifying genres match" + ], + "native_result_sha256": "d396eb9e2228319bef5d8fc562b60932dbeffddd1e97466b201f564881133764" + }, + { + "task_id": "IMDb--16", + "source_run": "task-16/run-01", + "execution": { + "version": "r2", + "recorded_code_head": "50bcce503637522d15bdafa533381204750f415f", + "code_identity": "recorded code commit plus frozen image inputs", + "image": "sha256:74aec29b73596a5a11ba43a4a1d0b22b82a473b4d36fcb232c2235d83d4258e9", + "asset_revision": "e70f49d8b6d0f32c940688e8e53e7c558f57a788", + "asset_archive_sha256": "5199d6d2601d070e09b235a149855435f8bb121c768ee5ab36fd6118bf830b74", + "seed_sha256": "69f849b9c61fb71349958fdbedc301162881e3a171eb65098c2e5f6623b82471", + "source_inputs_manifest_sha256": "f8c80d0265456e664e15c174f15523b52b5dff0070dcf90365041946ebdee4eb", + "model": "GPT-6 (Codex; inherited runner model)", + "harness": "CUA + durable-recorder.mjs", + "harness_sha256": "9c4843ea68c52dd44acaff1efcc654183ba916428be764354c0ce937a5771e3b" + }, + "original_manifest_sha256": "3e6fafc3831abdade50369aee405d87e68c21580b33835564b2a28ffa331368d", + "original_manifest_size": 12617, + "recorded_step_count": 11, + "successful_substantive_action_count": 10, + "successful_substantive_step_indices": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "excluded_steps": { + "observe": [ + 10 + ], + "scroll": [], + "failed": [] + }, + "recorded_failures": [], + "before_db_sha256": "055663a1f0992fbf26da98b47e820707cbfad21bacd3f27fb996698c79b2e105", + "after_db_sha256": "4a8de27d3f6f4d07aaf023cadc3e97c49e3604dd371af406468341e15dd80228", + "ui_path_summary": "Sign in → Carol’s Watchlist → Fight Club → personal rating selection → account → My ratings.", + "visited_relative_routes": [ + "/", + "/login", + "/", + "/list/watchlist", + "/title/tt0137523", + "/account", + "/list/ratings" + ], + "final_answer": "Selected Fight Club (1999), the most recently released Crime movie in Carol’s initial Watchlist with a personal rating other than 8/10 (it was 9/10). Set Carol’s personal rating for Fight Club to 8/10 and confirmed the Fight Club 1999 row shows 8/10 on My ratings.", + "representative_image": { + "path": "IMDb--16-final.jpg", + "sha256": "8467262bdeb9e7f40a45b4683bbe68931d2aac60c70e9ee5450afd8d2993649c", + "size": 53344, + "original_jpeg_path": "screenshots/step_011.source.jpg", + "original_png_path": "screenshots/step_011.png", + "original_png_sha256": "3fe0e3960e85fc2346bb949a6502fabf1c56bb8cfd3fd165943612e08a50c9fd", + "original_png_size": 151096, + "width": 1470, + "height": 780, + "trajectory_step": 10, + "image_role": "final recorded after-action capture", + "copy_method": "original JPEG bytes; no transcoding" + }, + "scorer_result": "PASS", + "claude_result": "PASS", + "runner_prior_ui_context": { + "prior_answer_knowledge": "No hidden answers, implementation, evaluator or prior review accessed; own preceding pilot UI runs IMDb--3 and IMDb--17 are known.", + "prior_own_ui_runs": [ + "IMDb--3", + "IMDb--17", + "IMDb--0", + "IMDb--2", + "IMDb--4", + "IMDb--7", + "IMDb--8", + "IMDb--9", + "IMDb--10", + "IMDb--12", + "IMDb--14", + "IMDb--15" + ] + }, + "scorer_evidence": [ + "Carol's initial eligible Watchlist selects the rated movie; only its personal rating changed to 8", + "Local rating action followed by My ratings observed; reported title and year match" + ], + "native_result_sha256": "97eccdc9a7ef25e58263ccdf7d4ca26613f7e54f3d4f3ce3662bcfde0aeb5a4f" + }, + { + "task_id": "IMDb--17", + "source_run": "task-17/pilot-01", + "execution": { + "version": "r1", + "recorded_code_head": "ebe92f0888c5cf888fa40bd46f5b535ad01faa57", + "code_identity": "base commit plus frozen working inputs", + "image": "sha256:ee00ec866b5379fe6176c01e91e425ee527241127e86b5fc7b5c65757e3836fc", + "asset_revision": "4d5709e171d7c40fc742727adfa98b04b9023039", + "asset_archive_sha256": "80fbfed63f66312e2a0c853972d17652c4ada8b2790cda8c4d7286ee667cb9b2", + "seed_sha256": "27558f13a9dd3b003435463ceaad8538e2b6f9fc5095c3bd6831666fecaab5d9", + "source_inputs_manifest_sha256": "d069f421aabe831e681e6bf66423428b4080a93405a6d1bd6ca29d0afb6d4e8b", + "model": "GPT-6 (Codex; inherited runner model)", + "harness": "CUA + durable-recorder.mjs", + "harness_sha256": "9c4843ea68c52dd44acaff1efcc654183ba916428be764354c0ce937a5771e3b" + }, + "original_manifest_sha256": "f001c604b1e52377bf70d16061b250e2160f9025750adf3156c9ad7bd26a3c0d", + "original_manifest_size": 14343, + "recorded_step_count": 14, + "successful_substantive_action_count": 12, + "successful_substantive_step_indices": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12 + ], + "excluded_steps": { + "observe": [ + 0, + 13 + ], + "scroll": [], + "failed": [] + }, + "recorded_failures": [], + "before_db_sha256": "3df29298bafabe3ce6760e2f6daf10f491b6fde08ccf3f347cb0cb89b01fb3d9", + "after_db_sha256": "d78494c2ea5cc5c25872cff770cf68b52133ff7d6799103a0f258f9204e28e52", + "ui_path_summary": "Sign in → Interstellar search → title → written review form → user reviews.", + "visited_relative_routes": [ + "/", + "/login", + "/", + "/search?s=all&q=Interstellar", + "/title/tt0816692", + "/title/tt0816692/review", + "/title/tt0816692/reviews" + ], + "final_answer": "Signed in as alice.j@test.com (Alice Johnson) and posted one written review for Interstellar (2014). Headline: Brilliant sci-fi epic. Review rating: 10/10. Body: A moving and ambitious science-fiction adventure, with stunning visuals, memorable music, and a powerful story about family and exploration. Confirmed the new review appears on Interstellar’s user reviews page, attributed to Alice Johnson, with the headline and 10/10 rating displayed.", + "representative_image": { + "path": "IMDb--17-final.jpg", + "sha256": "99792e1b7690421adca103e13774e84c4bbac664956ac098164f5cc506d3b992", + "size": 77231, + "original_jpeg_path": "screenshots/step_014.source.jpg", + "original_png_path": "screenshots/step_014.png", + "original_png_sha256": "5f76c95e242a01159ade27d4cba5d058850ffdb4819154fe1b8b25c9b483ca82", + "original_png_size": 212293, + "width": 1470, + "height": 780, + "trajectory_step": 13, + "image_role": "final recorded after-action capture", + "copy_method": "original JPEG bytes; no transcoding" + }, + "scorer_result": "PASS", + "claude_result": "PASS", + "runner_prior_ui_context": { + "prior_answer_knowledge": false, + "prior_ui_runs_in_this_session": [ + "IMDb--3: independent read-only run on separate origin; no IMDb--17 solution knowledge" + ] + }, + "scorer_evidence": [ + "Exactly one new Alice review matches the required Interstellar headline, rating and nonempty body", + "Local review submission followed by that movie's reviews page observed; no other business table changed" + ], + "native_result_sha256": "db6cd1340249e38fb14398b27e89de1b377daa967d2801f7e224b4d09711727f" + } + ], + "scorer": { + "code_commit": "a37df75a873842be0ed4510baab5dbff361ac9ad", + "inputs_sha256": "22817f45317b99dd93986c3ec19b6b9d9ce029897b039b4acd6cdbe0500cf21d", + "mode": "native eval_judge.py --verifier True; offline regrade of unchanged original runs", + "blind_review": "PASS_10_RETAINED_MAIN_RUNS" + }, + "independent_review_summary": "../independent-review-summary.json" +} diff --git a/review-reports/assets/pr33-imdb/tasks/task-table.md b/review-reports/assets/pr33-imdb/tasks/task-table.md new file mode 100644 index 00000000..a1e38583 --- /dev/null +++ b/review-reports/assets/pr33-imdb/tasks/task-table.md @@ -0,0 +1,28 @@ +# Recorded task excerpts + +These are the ten retained task executions. The runner was isolated from implementation, answer keys and scoring outputs, but retained its own preceding UI context, including the retired tasks. This is not a fresh-context model success-rate study. Each final JPEG is copied byte-for-byte from the original capture and hash-checked; a final image is an excerpt, not evidence of the entire run. + +All ten retained executions pass the native deterministic evaluator at commit `a37df75a873842be0ed4510baab5dbff361ac9ad`. Independent Claude review also passes **10/10** recorded executions; see the [summary and limits](../independent-review-summary.json). Full original evidence is retained privately. + +| Task | Executed version | Recorded steps | Successful substantive actions | Recorded issue | Final JPEG | +|---|---|---:|---:|---|---| +| IMDb--0 | r2 | 5 | 4 | None recorded | [Image](IMDb--0-final.jpg) | +| IMDb--2 | r2 | 6 | 4 | bookkeeping | [Image](IMDb--2-final.jpg) | +| IMDb--7 | r2 | 5 | 3 | None recorded | [Image](IMDb--7-final.jpg) | +| IMDb--9 | r2 | 10 | 6 | ui_action | [Image](IMDb--9-final.jpg) | +| IMDb--10 | r2 | 15 | 14 | None recorded | [Image](IMDb--10-final.jpg) | +| IMDb--12 | r2 | 11 | 9 | None recorded | [Image](IMDb--12-final.jpg) | +| IMDb--14 | r2 | 7 | 6 | None recorded | [Image](IMDb--14-final.jpg) | +| IMDb--15 | r2 | 14 | 12 | None recorded | [Image](IMDb--15-final.jpg) | +| IMDb--16 | r2 | 11 | 10 | None recorded | [Image](IMDb--16-final.jpg) | +| IMDb--17 | r1 | 14 | 12 | None recorded | [Image](IMDb--17-final.jpg) | + +Counts exclude observations, scrolling, failed actions and preparation/bookkeeping. Login form fills count as successful actions; no credential values are included. + +Task17 retains r1: base `ebe92f…` plus frozen working-input hashes, image `ee00ec…`, HF `4d5709…`. The other nine use r2: code `50bcce…`, image `74aec2…`, HF `e70f49…`. Full identifiers, original hashes, recorded final answers and prior UI context are in [task-results.json](task-results.json). + +Seven observed paths used at least five successful substantive actions (9/10/12/14/15/16/17). Tasks0/12/14 compare information. The observed paths do not prove a minimum over every legal route. Task10’s tied leaders and tasks15/16’s personal initial-state constraints support the design judgment that precision can challenge a frontier model; empirical difficulty remains **NOT_VERIFIED**. + +Tasks3/4/8 were removed after quality review: the first two resolve on the first matching title detail; task8 has a stable common-knowledge shortcut. The original thirteen runs and old scoring outputs remain unchanged. + +Recorded issues in this retained set: task2’s duplicate manifest attempt was rejected before a corrected helper call; task9 recorded one failed Type selector action and recovered using the observed control. See the structured records for those disclosures. diff --git a/sites/imdb/_health.py b/sites/imdb/_health.py new file mode 100644 index 00000000..b21acf55 --- /dev/null +++ b/sites/imdb/_health.py @@ -0,0 +1,3 @@ +"""Per-site health probe (optional, called by control_server).""" +def health(): + return {"ok": True, "site": "imdb"} diff --git a/sites/imdb/app.py b/sites/imdb/app.py new file mode 100644 index 00000000..9ad80897 --- /dev/null +++ b/sites/imdb/app.py @@ -0,0 +1,894 @@ +"""IMDb mirror — Flask app. + +Phase 1 (clone-website) rewrite: real catalog data from Playwright scraping, +scored token-overlap search, four benchmark users with the canonical +WebHarbor email scheme (alice.j / bob.c / carol.d / david.k). + +Mirrors the public, logged-out + logged-in browsing surface of imdb.com that +matters for web-agent benchmarks: title detail, name detail, search, charts +(Top 250 / Most Popular / Box Office), reviews, ratings, watchlist, news. +""" +import os +import json +import re +from datetime import datetime +from urllib.parse import unquote, urlsplit, urlunsplit + +from flask import (Flask, render_template, request, redirect, url_for, + flash, abort, jsonify, session) +from flask_sqlalchemy import SQLAlchemy +from flask_wtf.csrf import CSRFProtect +from email_validator import EmailNotValidError, validate_email +from flask_login import (LoginManager, UserMixin, login_user, logout_user, + login_required, current_user) +from werkzeug.security import generate_password_hash, check_password_hash +from sqlalchemy import func, desc + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +app = Flask(__name__) +app.config['SECRET_KEY'] = 'imdb-mirror-dev-secret' +app.config['SQLALCHEMY_DATABASE_URI'] = ( + f"sqlite:///{os.path.join(BASE_DIR, 'instance', 'imdb.db')}" +) +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +os.makedirs(os.path.join(BASE_DIR, 'instance'), exist_ok=True) + +db = SQLAlchemy(app) +csrf = CSRFProtect(app) +login_manager = LoginManager(app) +login_manager.login_view = 'login' +login_manager.login_message = 'Please sign in to use this feature.' + + +# --------------------------------------------------------------------------- +# Models +# --------------------------------------------------------------------------- + +title_genre = db.Table( + 'title_genre', + db.Column('title_id', db.Integer, db.ForeignKey('titles.id'), primary_key=True), + db.Column('genre_id', db.Integer, db.ForeignKey('genres.id'), primary_key=True), +) + + +class User(db.Model, UserMixin): + __tablename__ = 'users' + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(160), unique=True, nullable=False, index=True) + name = db.Column(db.String(120), nullable=False) + password_hash = db.Column(db.String(255), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + reviews = db.relationship('Review', backref='user', lazy=True, + cascade='all, delete-orphan') + ratings = db.relationship('UserRating', backref='user', lazy=True, + cascade='all, delete-orphan') + watchlist = db.relationship('WatchlistItem', backref='user', lazy=True, + cascade='all, delete-orphan') + + def set_password(self, pw): + self.password_hash = generate_password_hash(pw) + + def check_password(self, pw): + return check_password_hash(self.password_hash, pw) + + +class Title(db.Model): + __tablename__ = 'titles' + id = db.Column(db.Integer, primary_key=True) + tt_id = db.Column(db.String(20), unique=True, nullable=False, index=True) + title_type = db.Column(db.String(20), nullable=False, default='movie') + primary_title = db.Column(db.String(255), nullable=False, index=True) + original_title = db.Column(db.String(255), default='') + year = db.Column(db.Integer, index=True) + end_year = db.Column(db.Integer) + runtime_min = db.Column(db.Integer) + mpaa_rating = db.Column(db.String(10), default='') + plot_short = db.Column(db.String(500), default='') + plot = db.Column(db.Text, default='') + rating_avg = db.Column(db.Float, default=0.0, index=True) + num_votes = db.Column(db.Integer, default=0) + metascore = db.Column(db.Integer) + popularity_rank = db.Column(db.Integer, index=True) + top_rank = db.Column(db.Integer, index=True) + box_office_us = db.Column(db.BigInteger) + box_office_world = db.Column(db.BigInteger) + box_office_opening = db.Column(db.BigInteger) + budget = db.Column(db.BigInteger) + release_date = db.Column(db.String(20), default='') + country = db.Column(db.String(80), default='') + language = db.Column(db.String(80), default='') + poster_path = db.Column(db.String(255), default='') + taglines_json = db.Column(db.Text, default='[]') + + genres = db.relationship('Genre', secondary=title_genre, backref='titles', + lazy='joined') + credits = db.relationship('Credit', backref='title', lazy=True, + cascade='all, delete-orphan') + reviews = db.relationship('Review', backref='title', lazy=True, + cascade='all, delete-orphan') + + @property + def directors(self): + return [c for c in self.credits if c.role == 'director'] + + @property + def writers(self): + return [c for c in self.credits if c.role == 'writer'] + + @property + def cast(self): + return sorted([c for c in self.credits if c.role == 'actor'], + key=lambda c: c.billing_order or 999) + + def taglines(self): + try: + return json.loads(self.taglines_json or '[]') + except Exception: + return [] + + @property + def search_text(self): + gnames = ' '.join(g.name for g in self.genres) + people = ' '.join(c.person.name for c in self.credits[:8] if c.person) + return ' '.join([ + self.primary_title, self.original_title or '', + str(self.year or ''), gnames, people, self.plot_short or '', + ]).lower() + + +class Person(db.Model): + __tablename__ = 'persons' + id = db.Column(db.Integer, primary_key=True) + nm_id = db.Column(db.String(20), unique=True, nullable=False, index=True) + name = db.Column(db.String(160), nullable=False, index=True) + birth_year = db.Column(db.Integer) + death_year = db.Column(db.Integer) + birth_place = db.Column(db.String(200), default='') + bio = db.Column(db.Text, default='') + primary_profession = db.Column(db.String(200), default='') + photo_path = db.Column(db.String(255), default='') + known_for_json = db.Column(db.Text, default='[]') + + credits = db.relationship('Credit', backref='person', lazy=True) + + def known_for(self): + try: + tt_ids = json.loads(self.known_for_json or '[]') + except Exception: + return [] + return Title.query.filter(Title.tt_id.in_(tt_ids)).all() + + +class Genre(db.Model): + __tablename__ = 'genres' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(50), unique=True, nullable=False) + slug = db.Column(db.String(50), unique=True, nullable=False, index=True) + + +class Credit(db.Model): + __tablename__ = 'credits' + id = db.Column(db.Integer, primary_key=True) + title_id = db.Column(db.Integer, db.ForeignKey('titles.id'), nullable=False, index=True) + person_id = db.Column(db.Integer, db.ForeignKey('persons.id'), nullable=False, index=True) + role = db.Column(db.String(20), nullable=False) + character = db.Column(db.String(160), default='') + billing_order = db.Column(db.Integer) + + +class Review(db.Model): + __tablename__ = 'reviews' + id = db.Column(db.Integer, primary_key=True) + title_id = db.Column(db.Integer, db.ForeignKey('titles.id'), nullable=False, index=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True) + rating = db.Column(db.Integer) + headline = db.Column(db.String(160), nullable=False) + body = db.Column(db.Text, nullable=False) + helpful_count = db.Column(db.Integer, default=0) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + is_seed = db.Column(db.Boolean, default=False) + + +class UserRating(db.Model): + __tablename__ = 'user_ratings' + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True) + title_id = db.Column(db.Integer, db.ForeignKey('titles.id'), nullable=False, index=True) + rating = db.Column(db.Integer, nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + __table_args__ = (db.UniqueConstraint('user_id', 'title_id'),) + + +class WatchlistItem(db.Model): + __tablename__ = 'watchlist_items' + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True) + title_id = db.Column(db.Integer, db.ForeignKey('titles.id'), nullable=False, index=True) + added_at = db.Column(db.DateTime, default=datetime.utcnow) + __table_args__ = (db.UniqueConstraint('user_id', 'title_id'),) + + +class NewsItem(db.Model): + __tablename__ = 'news_items' + id = db.Column(db.Integer, primary_key=True) + headline = db.Column(db.String(255), nullable=False) + summary = db.Column(db.Text, default='') + source = db.Column(db.String(120), default='') + published_at = db.Column(db.String(40), default='') + category = db.Column(db.String(40), default='') + related_tt = db.Column(db.String(20), default='') + + +class HomeFeature(db.Model): + """Sourced homepage snapshot, separate from the benchmark catalog.""" + __tablename__ = 'home_features' + id = db.Column(db.String(80), primary_key=True) + kind = db.Column(db.String(30), nullable=False) + position = db.Column(db.Integer, nullable=False) + heading = db.Column(db.Text, nullable=False) + subtitle = db.Column(db.Text, default='') + image_path = db.Column(db.Text, default='') + poster_path = db.Column(db.Text, default='') + source_url = db.Column(db.Text, nullable=False) + captured_at = db.Column(db.String(40), nullable=False) + payload = db.Column(db.JSON, nullable=False) + + +@login_manager.user_loader +def load_user(uid): + return db.session.get(User, int(uid)) + + +# --------------------------------------------------------------------------- +# Scored token-overlap search (skill: NEVER strict AND) +# --------------------------------------------------------------------------- + +STOP_WORDS = { + 'the','a','an','in','on','at','to','for','of','and','or','is','it','by', + 'with','this','that','from','about','into','as','be','are','was','were', +} + + +def _tokenize(q): + return [t.lower() for t in re.split(r'\W+', q or '') + if t and len(t) > 1 and t.lower() not in STOP_WORDS] + + +def scored_title_search(query, limit=50): + toks = _tokenize(query) + if not toks: + return [] + rows = Title.query.all() + hits = [] + for t in rows: + text = t.search_text + score = sum(1 for tk in toks if tk in text) + if score: + hits.append((score, -(t.num_votes or 0), t)) + hits.sort(key=lambda x: (-x[0], x[1])) + return [h[2] for h in hits[:limit]] + + +def scored_person_search(query, limit=50): + toks = _tokenize(query) + if not toks: + return [] + rows = Person.query.all() + hits = [] + for p in rows: + name_lc = p.name.lower() + body = (' ' + (p.primary_profession or '') + ' ' + + (p.birth_place or '') + ' ' + (p.bio or '')).lower() + # Name matches weighted 10x; substring of full-name another +5. + score = 0 + for tk in toks: + if tk in name_lc: + score += 10 + if tk in body: + score += 1 + if all(tk in name_lc for tk in toks): + score += 50 # exact full-name superiority + if score: + hits.append((score, name_lc, p)) + hits.sort(key=lambda x: (-x[0], x[1])) + return [h[2] for h in hits[:limit]] + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _get_title_or_404(tt_id): + t = Title.query.filter_by(tt_id=tt_id).first() + if not t: + abort(404) + return t + + +def _get_person_or_404(nm_id): + p = Person.query.filter_by(nm_id=nm_id).first() + if not p: + abort(404) + return p + + +def _user_watchlist_ids(): + if not current_user.is_authenticated: + return set() + return {w.title_id for w in current_user.watchlist} + + +def _user_rating(title_id): + if not current_user.is_authenticated: + return None + r = UserRating.query.filter_by(user_id=current_user.id, title_id=title_id).first() + return r.rating if r else None + + +@app.context_processor +def inject_globals(): + return { + 'site_name': 'IMDb', + 'now_year': datetime.utcnow().year, + 'in_watchlist': _user_watchlist_ids(), + } + + +@app.template_filter('money') +def fmt_money(v): + if not v: + return '—' + if v >= 1_000_000_000: + return f'${v/1_000_000_000:.1f}B' + if v >= 1_000_000: + return f'${v/1_000_000:.1f}M' + if v >= 1_000: + return f'${v/1_000:.1f}K' + return f'${v}' + + +@app.template_filter('compact_votes') +def fmt_votes(v): + if not v: + return '0' + if v >= 1_000_000: + return f'{v/1_000_000:.1f}M' + if v >= 1_000: + return f'{v/1_000:.1f}K' + return str(v) + + +# --------------------------------------------------------------------------- +# Routes +# --------------------------------------------------------------------------- + +@app.route('/') +def index(): + top_picks = (Title.query + .filter(Title.top_rank.isnot(None)) + .order_by(Title.top_rank.asc()) + .limit(10).all()) + trending = (Title.query + .filter(Title.popularity_rank.isnot(None)) + .order_by(Title.popularity_rank.asc()) + .limit(10).all()) + in_theaters = (Title.query + .filter(func.date(Title.release_date).isnot(None)) + .order_by(desc(func.date(Title.release_date)), Title.id) + .limit(8).all()) + fan_favorites = (Title.query.filter(Title.num_votes > 0) + .order_by(desc(Title.num_votes), Title.id).limit(12).all()) + box_office = (Title.query.filter(Title.box_office_us.isnot(None)) + .order_by(desc(Title.box_office_us), Title.id).limit(6).all()) + latest_news = NewsItem.query.order_by(desc(NewsItem.published_at), desc(NewsItem.id)).limit(5).all() + features = {} + for feature in HomeFeature.query.order_by(HomeFeature.position, HomeFeature.id): + features.setdefault(feature.kind, []).append(feature) + watchlist = [] + if current_user.is_authenticated: + watchlist = (Title.query.join(WatchlistItem).filter( + WatchlistItem.user_id == current_user.id).order_by( + desc(WatchlistItem.added_at), WatchlistItem.id).limit(12).all()) + people = (Person.query.join(Credit).filter(Person.photo_path != '') + .group_by(Person.id).order_by(desc(func.count(Credit.id)), Person.id).limit(12).all()) + interests = Genre.query.order_by(Genre.name).all() + recent_ids = session.get('recent_titles', [])[:12] + recent_map = {t.tt_id: t for t in Title.query.filter(Title.tt_id.in_(recent_ids)).all()} + return render_template('index.html', + top_picks=top_picks, + trending=trending, + in_theaters=in_theaters, fan_favorites=fan_favorites, + box_office=box_office, + latest_news=latest_news, features=features, watchlist=watchlist, + people=people, interests=interests, + recent_titles=[recent_map[key] for key in recent_ids if key in recent_map]) + + +@app.route('/feature/') +def home_feature(feature_id): + feature = db.get_or_404(HomeFeature, feature_id) + source_id = feature.payload.get('source_id', feature.payload.get('title_id', '')) + title = Title.query.filter_by(tt_id=source_id).first() if source_id.startswith('tt') else None + person = Person.query.filter_by(nm_id=source_id).first() if source_id.startswith('nm') else None + feature_items = feature.payload.get('items', []) + item_ids = [item.get('source_id') for item in feature_items if item.get('source_id')] + catalog_ids = {row.tt_id for row in Title.query.filter(Title.tt_id.in_(item_ids)).all()} if item_ids else set() + related = (HomeFeature.query.filter(HomeFeature.kind == feature.kind, + HomeFeature.id != feature.id).order_by(HomeFeature.position, HomeFeature.id).limit(6).all()) + return render_template('home_feature.html', feature=feature, catalog_title=title, + catalog_person=person, related=related, + feature_items=feature_items, catalog_ids=catalog_ids) + + +@app.route('/feature//title/') +def home_feature_title(feature_id, source_id): + feature = db.get_or_404(HomeFeature, feature_id) + item = next((candidate for candidate in feature.payload.get('items', []) + if candidate.get('source_id') == source_id), None) + if item is None: + abort(404) + catalog_title = Title.query.filter_by(tt_id=source_id).first() + return render_template('home_feature_title.html', feature=feature, item=item, + catalog_title=catalog_title) + + +HOME_COLLECTIONS = { + 'starmeter': ('Trending people', 'STARmeter ranking captured from IMDb; not a live popularity chart.'), + 'streaming': ('Explore what’s streaming', 'Service availability captured from IMDb; playback is not bundled.'), + 'tv_schedule': ('Current & upcoming TV shows', 'Episode and season dates displayed in the captured homepage.'), + 'birthday': ('Born today', 'People born on the snapshot date, not the current system date.'), +} + + +@app.route('/discover/') +def home_collection(collection): + if collection not in HOME_COLLECTIONS: + abort(404) + heading, description = HOME_COLLECTIONS[collection] + items = HomeFeature.query.filter_by(kind=collection).order_by( + HomeFeature.position, HomeFeature.id).all() + return render_template('home_collection.html', heading=heading, description=description, + collection=collection, items=items) + + +@app.route('/recently-viewed/clear', methods=['POST']) +def clear_recently_viewed(): + session.pop('recent_titles', None) + return redirect(url_for('index')) + + +@app.route('/offline/') +def offline_service(service): + services = { + 'pro': 'IMDbPro', 'app': 'Get the IMDb app', 'streaming': 'Watch options', + 'help': 'Help', 'social': 'Follow IMDb on social', 'legal': 'Conditions and privacy', + 'awards': 'Awards & events', 'calendar': 'Current & upcoming TV shows', + 'birthdays': 'Born today', 'industry': 'IMDb industry services', + } + if service not in services: + abort(404) + return render_template('offline_service.html', heading=services[service], service=service) + + +@app.route('/title/') +def title_detail(tt_id): + t = _get_title_or_404(tt_id) + recent = session.get('recent_titles', []) + session['recent_titles'] = [tt_id] + [key for key in recent if key != tt_id][:11] + cast = t.cast[:15] + featured_reviews = (Review.query + .filter_by(title_id=t.id) + .order_by(desc(Review.is_seed), desc(Review.helpful_count)) + .limit(3).all()) + similar = (Title.query + .join(Title.genres) + .filter(Genre.id.in_([g.id for g in t.genres]), + Title.id != t.id) + .order_by(desc(Title.rating_avg)) + .limit(6).all()) + return render_template('title_detail.html', title=t, cast=cast, + featured_reviews=featured_reviews, + similar=similar, + user_rating=_user_rating(t.id)) + + +@app.route('/title//fullcredits') +def title_fullcredits(tt_id): + t = _get_title_or_404(tt_id) + return render_template('title_fullcredits.html', title=t) + + +@app.route('/title//reviews') +def title_reviews(tt_id): + t = _get_title_or_404(tt_id) + sort = request.args.get('sort', 'helpful') + q = Review.query.filter_by(title_id=t.id) + if sort == 'recent': + q = q.order_by(desc(Review.created_at)) + elif sort == 'rating': + q = q.order_by(desc(Review.rating)) + else: + q = q.order_by(desc(Review.helpful_count)) + return render_template('title_reviews.html', title=t, + reviews=q.all(), sort=sort) + + +@app.route('/title//review', methods=['GET', 'POST']) +@login_required +def title_write_review(tt_id): + t = _get_title_or_404(tt_id) + if request.method == 'POST': + headline = (request.form.get('headline') or '').strip() + body = (request.form.get('body') or '').strip() + rating = request.form.get('rating') + if not headline or not body: + flash('Headline and body are required.', 'error') + return render_template('title_write_review.html', title=t) + try: + rating = int(rating) if rating else None + except ValueError: + rating = 0 + if rating is not None and not 1 <= rating <= 10: + flash('Rating must be a whole number from 1 to 10.', 'error') + return render_template('title_write_review.html', title=t) + r = Review(title_id=t.id, user_id=current_user.id, + headline=headline[:160], body=body, + rating=rating) + db.session.add(r) + db.session.commit() + flash('Review posted.', 'success') + return redirect(url_for('title_reviews', tt_id=t.tt_id)) + return render_template('title_write_review.html', title=t) + + +@app.route('/title//rate', methods=['POST']) +@login_required +def title_rate(tt_id): + t = _get_title_or_404(tt_id) + try: + rating = int(request.form['rating']) + except (KeyError, ValueError): + flash('Invalid rating.', 'error') + return redirect(url_for('title_detail', tt_id=t.tt_id)) + if not 1 <= rating <= 10: + flash('Rating must be 1-10.', 'error') + return redirect(url_for('title_detail', tt_id=t.tt_id)) + existing = UserRating.query.filter_by(user_id=current_user.id, title_id=t.id).first() + if existing: + existing.rating = rating + else: + db.session.add(UserRating(user_id=current_user.id, title_id=t.id, rating=rating)) + db.session.commit() + flash(f'Rated {rating}/10.', 'success') + return redirect(url_for('title_detail', tt_id=t.tt_id)) + + +@app.route('/title//watchlist', methods=['POST']) +@login_required +def title_watchlist_toggle(tt_id): + t = _get_title_or_404(tt_id) + existing = WatchlistItem.query.filter_by(user_id=current_user.id, + title_id=t.id).first() + if existing: + db.session.delete(existing) + flash('Removed from Watchlist.', 'info') + else: + db.session.add(WatchlistItem(user_id=current_user.id, title_id=t.id)) + flash('Added to Watchlist.', 'success') + db.session.commit() + return redirect(_same_origin_return(url_for('title_detail', tt_id=t.tt_id))) + + +def _same_origin_return(fallback): + """Return only a same-origin path/query, never an authority or credentials.""" + referrer = request.referrer + if not referrer: + return fallback + decoded = unquote(referrer) + if '\\' in decoded or any(ord(char) < 32 or ord(char) == 127 for char in decoded): + return fallback + try: + target = urlsplit(referrer) + current = urlsplit(request.host_url) + + def origin(parts): + if parts.scheme not in ('http', 'https') or not parts.hostname: + return None + if parts.username is not None or parts.password is not None: + return None + port = parts.port + return parts.scheme, parts.hostname, port if port is not None else (443 if parts.scheme == 'https' else 80) + + if origin(target) is None or origin(target) != origin(current): + return fallback + except ValueError: + return fallback + path = target.path or '/' + if not path.startswith('/') or unquote(path).startswith('//'): + return fallback + return urlunsplit(('', '', path, target.query, '')) + + +@app.route('/name/') +def name_detail(nm_id): + p = _get_person_or_404(nm_id) + grouped = {'director': [], 'writer': [], 'actor': [], 'producer': [], 'composer': []} + for c in p.credits: + grouped.setdefault(c.role, []).append(c) + for k in grouped: + grouped[k].sort(key=lambda c: -(c.title.year or 0)) + return render_template('name_detail.html', person=p, grouped=grouped, + known_for=p.known_for()) + + +# Canonical IMDb search URL is /find?q= (URL realism, per WebHarbor doc). +# /search?q= remains as a WebHarbor convention alias. +@app.route('/find') +@app.route('/find/') +@app.route('/search') +def search(): + q = (request.args.get('q') or '').strip() + kind = request.args.get('s', 'all') + titles, people = [], [] + if q: + if kind in ('all', 'tt'): + titles = scored_title_search(q, limit=50) + if kind in ('all', 'nm'): + people = scored_person_search(q, limit=50) + return render_template('search.html', q=q, kind=kind, + titles=titles, people=people) + + +@app.route('/search/title') +def advanced_title_search(): + q = request.args + query = Title.query + selected_genres = q.getlist('genre') + title_type = q.get('title_type', '') + year_from = _search_year(q.get('year_from')) + year_to = _search_year(q.get('year_to')) + rating_min = q.get('rating_min', type=float) + sort = q.get('sort', 'popularity') + if title_type: + query = query.filter(Title.title_type == title_type) + if year_from: + query = query.filter(Title.year >= year_from) + if year_to: + query = query.filter(Title.year <= year_to) + if rating_min: + query = query.filter(Title.rating_avg >= rating_min) + if selected_genres: + query = (query.join(Title.genres) + .filter(Genre.slug.in_(selected_genres)) + .group_by(Title.id) + .having(func.count(Genre.id) == len(selected_genres))) + if sort == 'rating': + query = query.order_by(desc(Title.rating_avg)) + elif sort == 'votes': + query = query.order_by(desc(Title.num_votes)) + elif sort == 'year': + query = query.order_by(desc(Title.year)) + elif sort == 'box_office': + query = query.order_by(desc(Title.box_office_world)) + else: + query = query.order_by(Title.popularity_rank.asc().nulls_last()) + results = query.limit(100).all() + all_genres = Genre.query.order_by(Genre.name).all() + return render_template('advanced_search.html', + results=results, + all_genres=all_genres, + selected_genres=selected_genres, + title_type=title_type, + year_from=year_from, year_to=year_to, + rating_min=rating_min, sort=sort) + + +def _search_year(raw): + if raw is None or not raw.strip(): + return None + try: + year = int(raw) + except ValueError: + abort(400, description='Search year must be a whole number from 1 to 9999.') + if not 1 <= year <= 9999: + abort(400, description='Search year must be a whole number from 1 to 9999.') + return year + + +@app.route('/chart/top') +def chart_top(): + titles = (Title.query + .filter(Title.title_type == 'movie', Title.top_rank.isnot(None)) + .order_by(Title.top_rank.asc()) + .all()) + return render_template('chart.html', titles=titles, + chart_name='IMDb Top 250 Movies', + chart_slug='top', + description='As rated by regular IMDb voters.') + + +@app.route('/chart/toptv') +def chart_toptv(): + titles = (Title.query + .filter(Title.title_type == 'tvSeries', Title.top_rank.isnot(None)) + .order_by(Title.top_rank.asc()) + .all()) + return render_template('chart.html', titles=titles, + chart_name='Top 250 TV Shows', + chart_slug='toptv', + description='Highest-rated TV series of all time.') + + +@app.route('/chart/moviemeter') +def chart_moviemeter(): + titles = (Title.query + .filter(Title.popularity_rank.isnot(None)) + .order_by(Title.popularity_rank.asc()) + .limit(100).all()) + return render_template('chart.html', titles=titles, + chart_name='Most Popular Movies', + chart_slug='moviemeter', + description="IMDb users' most popular page views this week.") + + +@app.route('/chart/boxoffice') +def chart_boxoffice(): + titles = (Title.query + .filter(Title.box_office_us.isnot(None)) + .order_by(desc(Title.box_office_us)) + .limit(50).all()) + return render_template('chart.html', titles=titles, + chart_name='Domestic box office', + chart_slug='boxoffice', + description='Titles in this catalog, ranked by cumulative US & Canada gross.') + + +@app.route('/genre/') +def genre_browse(slug): + g = Genre.query.filter_by(slug=slug).first_or_404() + titles = (Title.query.join(Title.genres) + .filter(Genre.id == g.id) + .order_by(desc(Title.rating_avg)) + .limit(60).all()) + return render_template('genre_browse.html', genre=g, titles=titles) + + +@app.route('/list/watchlist') +@login_required +def my_watchlist(): + items = (WatchlistItem.query + .filter_by(user_id=current_user.id) + .order_by(desc(WatchlistItem.added_at)) + .all()) + titles = [db.session.get(Title, w.title_id) for w in items] + return render_template('watchlist.html', titles=titles) + + +@app.route('/list/ratings') +@login_required +def my_ratings(): + rs = (UserRating.query + .filter_by(user_id=current_user.id) + .order_by(desc(UserRating.created_at)) + .all()) + rows = [(r, db.session.get(Title, r.title_id)) for r in rs] + return render_template('my_ratings.html', rows=rows) + + +@app.route('/news') +def news_list(): + category = request.args.get('category', '') + query = NewsItem.query + if category: + query = query.filter_by(category=category) + items = query.order_by(desc(NewsItem.published_at), desc(NewsItem.id)).all() + categories = [row[0] for row in db.session.query(NewsItem.category).distinct().order_by(NewsItem.category)] + return render_template('news.html', items=items, categories=categories, category=category) + + +@app.route('/news/') +def news_detail(news_id): + item = db.get_or_404(NewsItem, news_id) + return render_template('news_detail.html', item=item) + + +@app.route('/register', methods=['GET', 'POST']) +def register(): + if current_user.is_authenticated: + return redirect(url_for('index')) + if request.method == 'POST': + email = (request.form.get('email') or '').strip().lower() + name = (request.form.get('name') or '').strip() + pw = request.form.get('password') or '' + if not email or not name or len(pw) < 6: + flash('Email, name and a 6+ character password are required.', 'error') + return render_template('register.html') + try: + validate_email(email, check_deliverability=False) + except EmailNotValidError: + flash('Enter a valid email address.', 'error') + return render_template('register.html') + if User.query.filter_by(email=email).first(): + flash('Account with that email already exists.', 'error') + return render_template('register.html') + u = User(email=email, name=name) + u.set_password(pw) + db.session.add(u) + db.session.commit() + login_user(u) + flash('Welcome to IMDb.', 'success') + return redirect(url_for('index')) + return render_template('register.html') + + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if current_user.is_authenticated: + return redirect(url_for('index')) + if request.method == 'POST': + email = (request.form.get('email') or '').strip().lower() + pw = request.form.get('password') or '' + u = User.query.filter_by(email=email).first() + if not u or not u.check_password(pw): + flash('Invalid email or password.', 'error') + return render_template('login.html') + login_user(u) + flash(f'Welcome back, {u.name}.', 'success') + return redirect(url_for('index')) + return render_template('login.html') + + +@app.route('/logout', methods=['POST']) +@login_required +def logout(): + logout_user() + flash('Signed out.', 'info') + return redirect(url_for('index')) + + +@app.route('/account') +@login_required +def account(): + review_count = Review.query.filter_by(user_id=current_user.id).count() + rating_count = UserRating.query.filter_by(user_id=current_user.id).count() + watch_count = WatchlistItem.query.filter_by(user_id=current_user.id).count() + return render_template('account.html', + review_count=review_count, + rating_count=rating_count, + watch_count=watch_count) + + +@app.errorhandler(404) +def not_found(e): + return render_template('404.html'), 404 + + +@app.route('/_health') +def health(): + return jsonify({'ok': True, 'site': 'imdb', + 'titles': Title.query.count(), + 'persons': Person.query.count(), + 'users': User.query.count()}) + + +# --------------------------------------------------------------------------- +# Bootstrap +# --------------------------------------------------------------------------- + +def _bootstrap(): + with app.app_context(): + db.create_all() + from seed_data import seed_all + seed_all(db, Title, Person, Genre, Credit, Review, UserRating, + WatchlistItem, User, NewsItem) + + +_bootstrap() + + +if __name__ == '__main__': + port = int(os.environ.get('PORT', 5000)) + app.run(host='0.0.0.0', port=port, debug=False) diff --git a/sites/imdb/docs/homepage-snapshot.md b/sites/imdb/docs/homepage-snapshot.md new file mode 100644 index 00000000..117fc892 --- /dev/null +++ b/sites/imdb/docs/homepage-snapshot.md @@ -0,0 +1,87 @@ +# Homepage snapshot + +The homepage now separates sourced editorial content from the existing benchmark +catalog. `home_features` contains nine trailer artwork previews, eight editorial +previews, four topic links, one episode spotlight (eight episode scores), five +news previews, 25 streaming titles, 37 TV schedule cards, 30 birthdays and the +captured top 100 STARmeter entries (219 source rows in total). The browser +archives were captured from IMDb on 2026-09-10 after their lazy-loaded sections +appeared. Media is served locally. `homepage-sources.json` and +`starmeter-sources.json` record the two archive hashes, capture times, exact image +URLs and local file hashes. The logo SVG comes from the homepage archive. + +The `What to Watch in September` editorial route has a separate, later source +observation because the homepage archive only carried its preview card. On +2026-09-12 the linked IMDb page contained 17 enhanced-list entries. Their title +IDs, displayed ratings and popularity, release context, editorial descriptions, +credits, exact media URLs and local media hashes are recorded in +`most-anticipated-2026-09-12.json`; `most-anticipated-import.json` records the +resulting seed hash. `seed_feature.py` validates the official source URLs and +local asset hashes before changing only that feature's JSON payload. The route +then exposes the full list and one local detail route per entry without hotlinks. + +The importer preserves all existing catalog and user-state tables. It replaces +only the content kinds present in the supplied archive: a pre-lazy-load archive +cannot erase already imported lower-page collections. Missing age/rating values +are left unknown, and dates/ages refer to the captured page rather than the +machine's clock. It is an +explicit build-time tool, not a startup or reset hook: + +```sh +python sites/imdb/seed_homepage.py source-home.webarchive \ + sites/imdb/instance_seed/imdb.db sites/imdb/static \ + --fetch-missing --manifest homepage-sources.json + +python sites/imdb/seed_homepage.py source-starmeter.webarchive \ + sites/imdb/instance_seed/imdb.db sites/imdb/static \ + --fetch-missing --manifest starmeter-sources.json +``` + +Save the logged-out official homepage with the browser's Web Archive option. +The importer first uses saved media. The optional flag retrieves only exact +image URLs in the saved page/data from IMDb's media CDN with TLS verification. +Downloaded media is cached in the build-only `scraped_data/homepage-media/` +directory so a transient network failure can be resumed without repeat downloads. +Do not commit the raw archive: it includes unrelated scripts and visitor metadata. +Package the updated seed and `static/images/home/` through the normal HF workflow. + +## Candidate status + +This is a local, unpublished candidate based on PR89's 07a67ae. The currently +pinned HF revision does **not** contain these new assets. Do not represent the +new homepage as available in that published revision or merge code without the +matching HF update. + +Desktop and 390px browser QA, the current 25-site reuse smoke, and a guided UI +replay of all 20 affected tasks are complete. The guided replay is impact +regression evidence, not a new independent exploration or blind review. Owner +full-page visual acceptance and a matching immutable HF/code release are still +required; engineering and guided checks alone do not accept the visual replica. + +## Remaining fidelity gaps + +- Trailers have sourced stills, posters and durations, but no video files. +- Editorial/topic destinations show the sourced preview; complete lists, polls + and galleries are not yet mirrored. +- Episode scores are sourced; full episode pages and rating writes are not added. +- STARmeter shows the source-observed top 100 and local portraits. Entries that + also exist in the benchmark catalog link to local person pages; the others stay + source snapshot cards rather than inventing catalog biographies. +- Birthdays, streaming availability and TV schedule cards now have dated source + content and local collection/detail routes. Where the same IMDb ID exists in + the benchmark catalog, the detail links to its biography or title/Watchlist. + These snapshot cards do not invent rating/favorite writes for unseeded entities. +- Live showtimes, ticketing and streaming playback are still not available. +- Interest cards lack source landscape artwork. Catalog ranking cards, box-office + layout and typography still need source-aligned visual refinement. The new + domestic box-office module explicitly uses cumulative catalog totals, not + fabricated current-weekend earnings. Releases sort by parsed recorded dates; + fan favorites sort by vote count rather than reversing the top-picks row. +- App downloads, commercial services and social destinations remain offline + information pages. No remote tracking, account linking or payments are loaded. + +Task definitions and scoring contracts remain unchanged (20 candidates). +Shared navigation and homepage entry paths changed. Impact-specific guided UI +revalidation now passes 20/20 with deterministic verifier PASS and reset after +each task; the previous 20 independent runs and verdicts remain immutable +historical evidence and are not replaced by this guided replay. diff --git a/sites/imdb/docs/homepage-sources.json b/sites/imdb/docs/homepage-sources.json new file mode 100644 index 00000000..2ae5852d --- /dev/null +++ b/sites/imdb/docs/homepage-sources.json @@ -0,0 +1,812 @@ +{ + "archive_sha256": "e9fbd4bfc45d55e44672a48fed89932d85c6a11ec29ae77d2e44f3fe6ff5ba24", + "captured_at": "2026-09-10T13:37:06.251Z", + "features": 119, + "assets": [ + { + "path": "images/home/0b926616ec304dfb07a555e6.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMzA1ODU1YjUtMWY3Zi00NjRkLTlkOTQtZTk3OTk5NDMzMGI0XkEyXkFqcGc@._V1_QL75_UX760_CR0,0,760,428_.jpg", + "sha256": "0b926616ec304dfb07a555e699f230c580e9717d77c3c4a79027f802915b547a", + "bytes": 57442 + }, + { + "path": "images/home/b8f32e26ad1f9394fe9fed44.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYmNlOWIxY2EtODU5Yi00ZThlLWFkZWEtNjgwZmIzYjIzOWNlXkEyXkFqcGc@._CR1256,399,3274,1842_QL75_UX760_CR0,0,760,428_.jpg", + "sha256": "b8f32e26ad1f9394fe9fed440771d387f4823a4d6ce40fac3ccf38a61b1bf8ed", + "bytes": 26170 + }, + { + "path": "images/home/1ff67bbe2ed0bcc14850c7ca.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYjYyYTc0OTEtNDM2My00NjBjLWI5MWMtNmJiYWRhOWRhNDUyXkEyXkFqcGc@._V1_QL75_UX760_CR0,0,760,428_.jpg", + "sha256": "1ff67bbe2ed0bcc14850c7cae2c587c81cddd4328a87fdbcf4de890892e814b7", + "bytes": 35727 + }, + { + "path": "images/home/58a1971f0dff708705870268.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYzZhMDM0YTUtZGExZS00MjRkLWExYzMtYTBhOTNmOTVmNmI2XkEyXkFqcGc@._V1_QL75_UX760_CR0,0,760,428_.jpg", + "sha256": "58a1971f0dff708705870268dda0b41eb71d357372e4f911a9cf6ae2be53e5ad", + "bytes": 46732 + }, + { + "path": "images/home/7cb084837222e23a6fadc4d5.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYTFkMGM1NmQtZDA1Yi00ZGE1LThhZjAtOGM3MGZjYTFhZjhkXkEyXkFqcGc@._V1_QL75_UX760_CR0,0,760,428_.jpg", + "sha256": "7cb084837222e23a6fadc4d53feb60d0bbf89a23971cb9ca87e9a85f7b5e470d", + "bytes": 47602 + }, + { + "path": "images/home/73da59a624b57cbf7f821e11.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNmZmYjVjZWUtOGJjMi00N2ZhLTkzZjAtNjJlMThhNmVhNzk4XkEyXkFqcGc@._V1_QL75_UY428_CR0,0,760,428_.jpg", + "sha256": "73da59a624b57cbf7f821e11402ee9726c6e9dd8372562fba874f2b43cae5d6b", + "bytes": 44812 + }, + { + "path": "images/home/07d3e0402fb21572883240f6.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYjMyOTdjN2UtYTYyYS00ZTQ1LWEzYWQtMDMxMjkwYzBjMTkyXkEyXkFqcGc@._V1_QL75_UX760_CR0,0,760,428_.jpg", + "sha256": "07d3e0402fb21572883240f63cf2b0d57cdbd4051435f7b513362ab39b03b22c", + "bytes": 45219 + }, + { + "path": "images/home/7df4980e13f4a11f5a3064ad.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYTlkZDNjYzktYmZkYS00YzcyLTgyNzEtMDE4YjMyMDYwMjQ2XkEyXkFqcGc@._V1_QL75_UX760_CR0,0,760,428_.jpg", + "sha256": "7df4980e13f4a11f5a3064ad6b322981d14b23f8408d676105ecb1b1fb1be788", + "bytes": 58689 + }, + { + "path": "images/home/475796b0bca5a565548490e5.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYTAzMjQ0Y2MtMmNiNi00NWQ0LWJmOWItNjAyMmI1MTIxZGJlXkEyXkFqcGdeQXRyYW5zY29kZS13b3JrZmxvdw@@._CR8,99,943,531_QL75_UX1000_CR0,0,1000,563_.jpg", + "sha256": "475796b0bca5a565548490e55e19a608c9cc7b471c43a1edbf84a565ce772c18", + "bytes": 34856 + }, + { + "path": "images/home/74f86882bf8ae4d3aac86bca.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZDUwOWFlMWMtZjFkOC00YmE2LWI2MmUtNGFiMGE2MGUzZmNlXkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "sha256": "74f86882bf8ae4d3aac86bca1822c88bec82e1b3a8fd02c9b2e7780a78394d3a", + "bytes": 8097 + }, + { + "path": "images/home/89b604c647c40dc72339e344.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYzBjOTg2MGQtMDNhYS00ZjEwLTljYTgtYzU1ZTA5YmFmNjk0XkEyXkFqcGc@._CR7,103,1682,946_QL75_UY563_CR0,0,1000,563_.jpg", + "sha256": "89b604c647c40dc72339e344448039d75093f309ace0f324703206ccecae0b0b", + "bytes": 63413 + }, + { + "path": "images/home/e5800219af78e28a926d8532.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BN2U2MGRkMzAtZGYwMC00MzM4LThiMzQtNWEyZTcxNTJhNDA2XkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "e5800219af78e28a926d8532d70d14a916165ac69b1fe66da4f0f125b2b54a7c", + "bytes": 3794 + }, + { + "path": "images/home/561580153e48c88631b5cd62.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNTBhNDM0Y2MtMzJjOC00Y2JjLWI2MzUtODNlMTA4YmE2YTk3XkEyXkFqcGc@._CR120,11,1753,986_QL75_UY563_CR0,0,1000,563_.jpg", + "sha256": "561580153e48c88631b5cd62282b6258c838e021b1a9b1d59b11550a3c81646f", + "bytes": 26022 + }, + { + "path": "images/home/519883466f42223cd2b53f29.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjAxY2M0NzAtZDZmZS00OTMxLWExODEtZTBlYmZkZjNiNzQzXkEyXkFqcGc@._V1_QL75_UY414_CR67,0,280,414_.jpg", + "sha256": "519883466f42223cd2b53f298e2c75898ba3c037f2cb92de39731c4102d14c07", + "bytes": 7379 + }, + { + "path": "images/home/1851405656e21e46c1e15435.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BODdkNGU0YWQtNmMxMC00OWY4LWFlNDItMzIxZDg5M2VhODQ4XkEyXkFqcGc@._CR13,30,2963,1667_QL75_UX1000_CR0,0,1000,563_.jpg", + "sha256": "1851405656e21e46c1e15435be295133d653a5018ef1a2f6f7c6e1a188319d78", + "bytes": 38602 + }, + { + "path": "images/home/8f710f8e4b2a57f6f494ecbb.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZTRhNGJmY2YtYjNmOC00ZTA4LThjMzktYTEwYTdkZDE0OTllXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "8f710f8e4b2a57f6f494ecbb2bc49b403ef4524f321231cb1c4fa957ce6992c7", + "bytes": 19025 + }, + { + "path": "images/home/dfc53e0c0dd291523394fef5.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BOTRjZDUzYmItMjcwOS00ZDFiLWE5YmUtNjQ1MzNkNTY5MGQ5XkEyXkFqcGc@._CR56,35,3985,2242_QL75_UX1000_CR0,0,1000,563_.jpg", + "sha256": "dfc53e0c0dd291523394fef58cf3151c49948fc78b686c2cbe50b49fa315982f", + "bytes": 29708 + }, + { + "path": "images/home/b0064dfa59aaad875a484d3f.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNDFlZTA4ZDgtMmRkZi00NzhhLTk5NzgtZTFmNGYzMzIxNmE4XkEyXkFqcGc@._V1_QL75_UY414_CR26,0,280,414_.jpg", + "sha256": "b0064dfa59aaad875a484d3f1549e6a0ffe53986c0f7bf8028503a8d5baf3858", + "bytes": 12451 + }, + { + "path": "images/home/525d1cd449c3d78d74f4dba3.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZmE2ZDk3YjYtMTJkYi00ZTg5LTk4MGQtZWEwOWYwNjQ2ZGJlXkEyXkFqcGc@._CR505,14,2280,1282_QL75_UY563_CR0,0,1000,563_.jpg", + "sha256": "525d1cd449c3d78d74f4dba302e567db85bbbcc9a8ed1b6cfe29f6111190f8e7", + "bytes": 30965 + }, + { + "path": "images/home/ed53b04f13b499db15a003a6.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYmMwN2NjMmEtNzdmOS00MTQwLWJhMjMtNzI3ZmI0ZDg3NTUwXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "ed53b04f13b499db15a003a62fee7612f50e0930f00df6a5661ba1182d0fb69b", + "bytes": 3349 + }, + { + "path": "images/home/b8a9078b52c5742ec12ff711.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNjM2ZmY3NzUtZmE5Ni00NmM0LTg5N2UtNmQ4ZDg2N2E0NjMwXkEyXkFqcGdeQWRpZWdtb25n._CR37,130,1566,881_QL75_UX1000_CR0,0,1000,563_.jpg", + "sha256": "b8a9078b52c5742ec12ff711c968a5e0912e4df58f731597efffdad85ea01a4f", + "bytes": 43545 + }, + { + "path": "images/home/ca9bc4746a180a9d84ad9de2.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BN2E0YTg1M2YtOGQyYi00NDFkLTk3NzUtYmEyNTZhYThhZDQ0XkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "sha256": "ca9bc4746a180a9d84ad9de28494b1616556ee83e9700b2ed7a19e1946f5927d", + "bytes": 24772 + }, + { + "path": "images/home/6ac295a754f384fbd9bae7b4.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTQ1OWUwYjQtODgzYS00OTgzLTg5OWQtYjNjMzQ5ZDYyODI2XkEyXkFqcGc@._CR1712,1165,3724,2095_QL75_UX1000_CR0,0,1000,563_.jpg", + "sha256": "6ac295a754f384fbd9bae7b4aeb066d974ae4bde1f678bf26ce9823d73a8c65a", + "bytes": 113427 + }, + { + "path": "images/home/8686d88d870e9df8eb2808f6.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNzYzOWQ4ZjYtMmQyMS00Y2YwLWIxNDctZDlhNjcxZDE5MjVhXkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "sha256": "8686d88d870e9df8eb2808f62ec80fa67f546172f9c75746509c4a965d3369fb", + "bytes": 14943 + }, + { + "path": "images/home/260405b28ad6bde21894a682.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZGRjNDhiY2EtMTc2Ni00YjU2LWJiZTQtMGJiYTVlZjJmZTk0XkEyXkFqcGc@._CR503,136,4800,2700_QL75_UX1000_CR0,0,1000,563_.jpg", + "sha256": "260405b28ad6bde21894a68291ce026f03bb8316f467b3b0ec51d08c8f822595", + "bytes": 40575 + }, + { + "path": "images/home/df2a3c1ce5914b875264445c.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMzgxNzUwZTctMzliNi00MDUwLWE4YzctNjgwMDE2OWQwNzMxXkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "sha256": "df2a3c1ce5914b875264445cb63ad20501feb42411bbf74f1c8f6eae99753857", + "bytes": 20856 + }, + { + "path": "images/home/bf7f7a7e2c21d696d64bbb47.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMzdhNjMwYjAtNDFlYS00NWYyLTk4NzQtMmJjMzQ5NDk1NjMwXkEyXkFqcGc@._V1_.jpg", + "sha256": "bf7f7a7e2c21d696d64bbb4733c204f3748d0d2f0f3da01c81d9ba7ac1383728", + "bytes": 482591 + }, + { + "path": "images/home/8de5c6835592ce4d885b0dad.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTY1MGE2Y2YtMDYzZS00NzM0LWE4YjgtMmRjMmUzOWQ0ZDJkXkEyXkFqcGc@._V1_.jpg", + "sha256": "8de5c6835592ce4d885b0dadb170b6269e65b80d1215e56604b9d097da2c250c", + "bytes": 173953 + }, + { + "path": "images/home/7ab2427baad46c2e11958469.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BOWQwZmMyNTYtZmVlZC00MzdkLTk0MjMtNGFiYzQ4Yjc4OGFmXkEyXkFqcGc@._V1_.jpg", + "sha256": "7ab2427baad46c2e11958469d95e1899cf988c8dec6dd58a4a66a8039df944ab", + "bytes": 441886 + }, + { + "path": "images/home/e18d9ff4d07bd72eab35450f.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BM2ViNDM5MzEtNTEyZS00NzM0LWIyYzctNDk5MmU3MzI2YTFkXkEyXkFqcGc@._V1_.jpg", + "sha256": "e18d9ff4d07bd72eab35450f7d57f329faa22819be681dcbca41266d8a7642f2", + "bytes": 237563 + }, + { + "path": "images/home/19e3b7a88a6559bee714f98d.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNTAxNjQ0N2QtNGE3MS00MzI1LTllMDctNzdmN2MxNTZjNDYyXkEyXkFqcGc@._V1_.jpg", + "sha256": "19e3b7a88a6559bee714f98d49597086b27985d45fac438c83c312b3abfb0cdb", + "bytes": 534092 + }, + { + "path": "images/home/ac2b50df915706fd5a1c06fb.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNmJlZmY0ZjAtMzZjYy00NWZjLWE5ZmYtOWZlZjg1MGQ2MGUyXkEyXkFqcGc@._V1_.jpg", + "sha256": "ac2b50df915706fd5a1c06fb26be7897dead671ec9be4bb0ebf7626c20b5c14d", + "bytes": 222936 + }, + { + "path": "images/home/5d37833f69678f666aee7cdb.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNzJkOTZmYjItOThjNS00ZTk1LWIzYmUtNjU5YzQxMTkzM2Q5XkEyXkFqcGc@._V1_.jpg", + "sha256": "5d37833f69678f666aee7cdb12b69633517a0f6d6571aafe71e93dc079416998", + "bytes": 2383525 + }, + { + "path": "images/home/6b4b33100a5f18ff70ff0b49.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BOTI5MGNlM2YtYjc2YS00OTVmLWIzNDUtYzlkNmJlYmNjMDY2XkEyXkFqcGc@._V1_.jpg", + "sha256": "6b4b33100a5f18ff70ff0b492cceac89bae877c07bd9e85670e7f02018816fa6", + "bytes": 328182 + }, + { + "path": "images/home/b6e4b4c155c5a047241a2f5a.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMzhhZDFhOGYtZjJjOC00MDlhLThiMmEtYzFhYWYzMWQyNTBkXkEyXkFqcGc@._V1_QL75_UY414_CR8,0,280,414_.jpg", + "sha256": "b6e4b4c155c5a047241a2f5a1f196432d4869ae0dd4213ddca2c5300c172c2ff", + "bytes": 15900 + }, + { + "path": "images/home/741fa6f18a877cf184ddca71.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNmY3YWUyYzItYzViZC00OWZkLThmZjYtYzA3OGVkNDA3MTBmXkEyXkFqcGc@._V1_QL75_UY266_CR43,0,180,266_.jpg", + "sha256": "741fa6f18a877cf184ddca714bf116df980b1746e69afc482789c3e1a2006570", + "bytes": 5813 + }, + { + "path": "images/home/031c0d7d1220f6779431293e.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BODgxNThiNmItNWVkNi00YTRkLTgzZDItZGZlYmZkYjkxNTY2XkEyXkFqcGc@._V1_QL75_UY178_CR29,0,120,178_.jpg", + "sha256": "031c0d7d1220f6779431293e17e1f6ce053b719b08f49184938690095f336b98", + "bytes": 3722 + }, + { + "path": "images/home/272b306601f9f599dc8942dd.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BODVmMjVkMDUtZTQxNi00ODFhLTk4OWItNTc3Yzk4ZGFmZDMzXkEyXkFqcGc@._V1_QL75_UY178_CR73,0,120,178_.jpg", + "sha256": "272b306601f9f599dc8942dda0c24813c1c9b49dad73e7dc578c457167492a16", + "bytes": 2345 + }, + { + "path": "images/home/d8b33708bf5caac9eecd605a.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYjhjY2QwZGYtZDM5Mi00YWUxLTg0NGMtZTgwYjM5NDU3MmJkXkEyXkFqcGc@._V1_QL75_UY178_CR73,0,120,178_.jpg", + "sha256": "d8b33708bf5caac9eecd605a37a0a83bb5a200ee980f85025181636a36c11a41", + "bytes": 4203 + }, + { + "path": "images/home/41279c86ec7a1c6e16dc8bad.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BODYxNTg2OGYtMjVkYi00NDk3LWI3MzctMGIwZDkyZGYyNDEzXkEyXkFqcGc@._V1_QL75_UY178_CR73,0,120,178_.jpg", + "sha256": "41279c86ec7a1c6e16dc8bad0157762c1767d478886ae5412268d0cf8dedba00", + "bytes": 4063 + }, + { + "path": "images/home/35edb49dabcf6a511e7e044a.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMmE0YTA3MTItMjhiNC00MGQ3LTllZDktNGNiZWY1OWFmNmM1XkEyXkFqcGc@._V1_QL75_UY414_CR26,0,280,414_.jpg", + "sha256": "35edb49dabcf6a511e7e044a07b4089b96245b6ccef7a954f2045429a8e3ff9b", + "bytes": 15463 + }, + { + "path": "images/home/4f70bbbfbed0e75779546ea8.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYzgzYzVhY2QtNjIzNi00Zjk5LWE1ZmQtYmJkNGY3YmExNDdlXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "4f70bbbfbed0e75779546ea8ed0019ae948f9e8b9131e0a94b25996c7ebf663e", + "bytes": 19337 + }, + { + "path": "images/home/16f266f4b0289edaa72f7c0f.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BOWRjMzIxNDMtYTE4Ni00MDMwLTkxYmUtYmI3MGU5NTZiYmFhXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "16f266f4b0289edaa72f7c0f2c254343dc0eb23f7b12cda4767bde0aa0d0ad2c", + "bytes": 18462 + }, + { + "path": "images/home/5534583a5b22e55ae58ded56.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTQ5OTgzOTczM15BMl5BanBnXkFtZTcwMDM2OTY4MQ@@._V1_QL75_UY414_CR15,0,280,414_.jpg", + "sha256": "5534583a5b22e55ae58ded569f9d79210fa36f7c7d5b3743d5b3a5c9216a6aa7", + "bytes": 16690 + }, + { + "path": "images/home/05520fca274801cc7ebed1b2.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNTkwNzJiYTctNzI3NC00NjE1LTlhYjktY2Q5MTdmMWFmNzcxXkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "sha256": "05520fca274801cc7ebed1b2f1a9cbebbc6124e2763d31fd80859bd782a2daf9", + "bytes": 21312 + }, + { + "path": "images/home/40b2a826facf98a1f39b9fcf.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjZlMjQxN2MtYjAyOS00YjliLWE3NjEtNDZjOWI5NTAxNTY2XkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "40b2a826facf98a1f39b9fcf7d72901e349f44fbddb419a850d1c07b8e73732a", + "bytes": 21513 + }, + { + "path": "images/home/9ac832bf67b9b27e2e217edc.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYmI3ZGMzNWUtZjdmZC00YjNlLWE0NDktMTU3OTBhNjM4YzY0XkEyXkFqcGc@._V1_QL75_UY414_CR26,0,280,414_.jpg", + "sha256": "9ac832bf67b9b27e2e217edcd7c3537bc3ecdda0cc6b47521fbb61eeaf670d1c", + "bytes": 23475 + }, + { + "path": "images/home/86a8704cae11a1bc59d7bd4d.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNzliYzgxN2YtNTgzNy00MDQ4LWJiYTQtNWYyNDgzNGRlYTdhXkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "sha256": "86a8704cae11a1bc59d7bd4db78bc72eb1814e47853f089e5c15ede3811ab68a", + "bytes": 32087 + }, + { + "path": "images/home/17981bc0b5b719f53d38c20c.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTJjYTFkM2EtZjBmNy00OTk2LTg0NTAtNzYxYzlmNjhkMzQ5XkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "sha256": "17981bc0b5b719f53d38c20cd07c1e1b59769fd325d942e7b16c7f9d3f655f99", + "bytes": 29052 + }, + { + "path": "images/home/11261bf68f354144f44d680b.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMDFmMGZmMGItNGRjNC00NjVjLWI5ODEtNzhjMTE5MmJhN2FkXkEyXkFqcGc@._V1_QL75_UY414_CR2,0,280,414_.jpg", + "sha256": "11261bf68f354144f44d680bbe82dc4370a0d2c6b6ac3381bc131b67d38db94c", + "bytes": 18312 + }, + { + "path": "images/home/ba87690465b71d5c4971fabe.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMDA0NzAzNDktYjBiOS00ZjFjLThhZTYtNzlkMDMzOTgxZTk5XkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "ba87690465b71d5c4971fabebcd2ef2c2233781533960a8227629f5206a5ce7e", + "bytes": 20900 + }, + { + "path": "images/home/f80282aad456e707a01e2252.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYjU3NjEwNTItMzc1Mi00Y2QyLTljNDItOTQ4YjU4NjZjN2EyXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "f80282aad456e707a01e2252c293493d100b4ba25d138517b9e42c1d96cd0d70", + "bytes": 23470 + }, + { + "path": "images/home/0a3c543990d48ff5ea53b93f.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BOGQ1M2Y4ZjMtNWRiNS00YTUzLTkxMzEtZjgzYmZjMGVlMzY5XkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "0a3c543990d48ff5ea53b93fa273a93c725320498b16f591382dbfa7e0d3dea7", + "bytes": 22587 + }, + { + "path": "images/home/1682a0374fae029795838ef0.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZjQwYzBlYzUtZjhhOS00ZDQ0LWE0NzAtYTk4MjgzZTNkZWEzXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "1682a0374fae029795838ef05aff7684b9993c1a6cd3cb7eaf9c425bbbcef1e7", + "bytes": 28786 + }, + { + "path": "images/home/5dea870db27df8cbe57564cb.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZTYxYTdjZjEtNTc0My00NDU2LTkwZmQtNjhhNmJiMzQ3Zjc1XkEyXkFqcGc@._V1_QL75_UY414_CR2,0,280,414_.jpg", + "sha256": "5dea870db27df8cbe57564cbb717cc8912795f7e9952dc3bd35894f0d3c3663f", + "bytes": 23572 + }, + { + "path": "images/home/d4c95c26138ab555711b5760.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNDAwNDljNWEtNWE1Zi00YjFlLWEzM2UtY2E3ZTM1MTcxMTcyXkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "sha256": "d4c95c26138ab555711b57602ba94e97f960ed5669d5fd48f923ff4a63c5f150", + "bytes": 34027 + }, + { + "path": "images/home/9ea95eb9fd864698f0d5262c.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMWUwZjJmYjctYTY3MC00ZTVlLWI1ZGEtNWQzMTFlMTcwMTY2XkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "9ea95eb9fd864698f0d5262c75b139afeaa1cea3845d45dbbc4be6fdb92f1f8f", + "bytes": 27655 + }, + { + "path": "images/home/f559aef525dc19e270d0a2cc.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTU0NGIwMGQtNzk1OC00MzcxLTk0NDYtYWYyMjA3Mzg0ZWFkXkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "sha256": "f559aef525dc19e270d0a2ccf455523b94eb305ada45419629e91819b4c95478", + "bytes": 14839 + }, + { + "path": "images/home/3128de68bb8ad7dde2afb395.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZjU4OWNiYzQtMzc1NS00NjZlLTgyYTctZWY4ZmEzMTkxYjA4XkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "3128de68bb8ad7dde2afb395e95edaa27bbcbf461a0a7216bd1d1e729d086abd", + "bytes": 18300 + }, + { + "path": "images/home/afff51be1f2fa3aa7fc834e4.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZTFjMzMxZTUtYTMyNy00OWNhLTk4ODQtNGI1NjI1NjJhMzc3XkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "afff51be1f2fa3aa7fc834e47466d7386ccee1ac3754cf3ead9b3c5bb73dc7ec", + "bytes": 20245 + }, + { + "path": "images/home/53989267a058dca0dff1b423.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMGRlZjk0OTUtYjhkZS00NTU3LWI5NDItODZiMjM1N2U0NGQwXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "53989267a058dca0dff1b42316691ce351b1038e3eb8c1a17ee4800b6340145a", + "bytes": 16283 + }, + { + "path": "images/home/16f44c28f5393929db38ef3d.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNTQ1MzI1ZWYtNWMxMC00NmU4LTlmMjItZjY1NWMyYTdjMjM4XkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "sha256": "16f44c28f5393929db38ef3dfb9af0d2833aef437f4a6937af0ff395e08201a6", + "bytes": 24564 + }, + { + "path": "images/home/5025eaaceece8c1a491f0765.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMGUxMjFkMDEtNGJkZC00MTUwLWFiZjUtYWZhZWYxNzE1ZDEwXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "5025eaaceece8c1a491f0765ede5493defc9dd9651ed9f06e570300a76111e42", + "bytes": 22678 + }, + { + "path": "images/home/e7dd476a69076d21868cb0a9.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjMzMTIzMTUwN15BMl5BanBnXkFtZTgwNjE0NTg0MTE@._V1_QL75_UX280_CR0,2,280,414_.jpg", + "sha256": "e7dd476a69076d21868cb0a9b328cbedbbf6bc410b0a58e6aea5895333c2559c", + "bytes": 29529 + }, + { + "path": "images/home/0b76666c898bb5313552814a.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMzk0YTExY2EtYzI1NS00M2ZlLWFjOGUtMTQyMDY1MmE2ZGViXkEyXkFqcGc@._V1_QL75_UY414_CR0,0,280,414_.jpg", + "sha256": "0b76666c898bb5313552814aebd116fcb564986440ff651f02b5fd33649e0e5b", + "bytes": 19839 + }, + { + "path": "images/home/00a9787909a058fd90858222.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMWFhNjY2Y2EtZWRhYy00OWIxLTlkZGUtMTU0MWMzZTU1ZTJlXkEyXkFqcGc@._V1_QL75_UY414_CR6,0,280,414_.jpg", + "sha256": "00a9787909a058fd90858222d2423f7bae77a9e3d01b2ab2e1bb25488fb59de7", + "bytes": 17220 + }, + { + "path": "images/home/b6e4b4c155c5a047241a2f5a.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMzhhZDFhOGYtZjJjOC00MDlhLThiMmEtYzFhYWYzMWQyNTBkXkEyXkFqcGc@._V1_QL75_UY414_CR8,0,280,414_.jpg", + "sha256": "b6e4b4c155c5a047241a2f5a1f196432d4869ae0dd4213ddca2c5300c172c2ff", + "bytes": 15900 + }, + { + "path": "images/home/2624a63caeac6e5c63147b7d.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZmFhYTRlZjMtYjAwZS00ZGY1LWI3ODYtYmQ2MTE5NjU2Y2E2XkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "sha256": "2624a63caeac6e5c63147b7d08d4d384aec60e1e9b9f86b601d8b491b6d3c14a", + "bytes": 25342 + }, + { + "path": "images/home/53989267a058dca0dff1b423.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMGRlZjk0OTUtYjhkZS00NTU3LWI5NDItODZiMjM1N2U0NGQwXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "53989267a058dca0dff1b42316691ce351b1038e3eb8c1a17ee4800b6340145a", + "bytes": 16283 + }, + { + "path": "images/home/51dba665b0014197a208c5f1.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BN2E5ODJmOTUtYTk0NS00NmFiLTk0MTgtZDRlNzE1YmExMjk4XkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "51dba665b0014197a208c5f1ab1f9a8322863fcad44d03fcac46edab0222fc5b", + "bytes": 15627 + }, + { + "path": "images/home/3530b28dc7f6cc0a3b18b3c0.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BN2U4YWMyZTMtMmEzMy00Yzk0LThiNTctNjE0YTc5ZDZhZTk5XkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "3530b28dc7f6cc0a3b18b3c075cdbedfb0511cc0bd97e0ee6b0d663d31e9e6fb", + "bytes": 15857 + }, + { + "path": "images/home/74c7f1533f3f3f261b6a887c.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZjk4YWU1MWQtYWE2Mi00NzEyLWFmNjUtMDE1MjE1ZTYyZDFjXkEyXkFqcGc@._V1_QL75_UY414_CR26,0,280,414_.jpg", + "sha256": "74c7f1533f3f3f261b6a887c6da828e577880338308a72e51d8e84e05195af5a", + "bytes": 21359 + }, + { + "path": "images/home/de549bd5e15e3e3b403e85af.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNDAwZWU0OTQtZmM3NC00N2M3LTk5YWItMTg2OGQ4YTEyOTkwXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "de549bd5e15e3e3b403e85afbe463e85d66949a3b89fd9b3b637539bfaaa233b", + "bytes": 24958 + }, + { + "path": "images/home/f531ad6fb46306e41fcb8af6.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BOTczMjM3MDUtYmZjMC00YzZhLWE4ODQtNjg5YWU4NTJjNzY2XkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "f531ad6fb46306e41fcb8af63f023e5ae19db226cfa153e502bb984f6b4a7cbe", + "bytes": 34618 + }, + { + "path": "images/home/71fad6b9d526cb408b7f8ed3.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjgyM2QzMjAtOGZjOS00OGFkLTkxZGYtMDJjZGM5MzIzYmM3XkEyXkFqcGc@._V1_QL75_UY414_CR7,0,280,414_.jpg", + "sha256": "71fad6b9d526cb408b7f8ed38500d1cfe0c70373f7d4403d197b3b4d50de0049", + "bytes": 23777 + }, + { + "path": "images/home/f5671c08bcae4c546226e2f6.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BM2ZjZDNlNDgtMjJlMy00NGRjLThlMGEtM2U4N2E4MDUwNjNmXkEyXkFqcGc@._V1_QL75_UY414_CR20,0,280,414_.jpg", + "sha256": "f5671c08bcae4c546226e2f6efe267e188e591af05dc3ef01c733e2af5c05dd6", + "bytes": 17741 + }, + { + "path": "images/home/16f266f4b0289edaa72f7c0f.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BOWRjMzIxNDMtYTE4Ni00MDMwLTkxYmUtYmI3MGU5NTZiYmFhXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "16f266f4b0289edaa72f7c0f2c254343dc0eb23f7b12cda4767bde0aa0d0ad2c", + "bytes": 18462 + }, + { + "path": "images/home/11071dd32b931851e3a8e2bf.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjg1ZTAyNTItMTRlMy00ZmFkLTkzYTAtM2NlYjk4YWYzNzM4XkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "sha256": "11071dd32b931851e3a8e2bf6e42941f7ce329f706f5fff1f70be1166e8c2705", + "bytes": 13765 + }, + { + "path": "images/home/d036660c29b77b8cfda36f6e.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTNjNGU4NTUtYmVjMy00YjRiLTkxMWUtNzZkMDNiYjZhNmViXkEyXkFqcGc@._V1_QL75_UY414_CR11,0,280,414_.jpg", + "sha256": "d036660c29b77b8cfda36f6e88003a8dd94453e151ae1c571d251cdfa62fd017", + "bytes": 35155 + }, + { + "path": "images/home/7e9edb3dd761b4c4aa3ab1ef.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYWQwNjk3MDItNDAxMS00YTQ2LWEyNDctMGYyZTE5OGQxNGQ1XkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "7e9edb3dd761b4c4aa3ab1ef09118c5d9621dfdf3f4c571c7c31d36c6414c904", + "bytes": 34256 + }, + { + "path": "images/home/10020b089dd43dd9141d0078.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYWI0ZjgxYWYtODZmNi00ZWI0LWI4MmEtZDBmOTNmMTRlMjY0XkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "sha256": "10020b089dd43dd9141d0078c9740213841fda333b31d96bdf6d1a9343febb84", + "bytes": 25736 + }, + { + "path": "images/home/30c52d5acd51852f61c6ca3d.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZjkxZWUzNGEtNWFjZi00MDJmLWI2ODctMDRiZTdhY2RiNmZkXkEyXkFqcGc@._V1_QL75_UY414_CR26,0,280,414_.jpg", + "sha256": "30c52d5acd51852f61c6ca3d5f67973feac845eab49f9c32578bc233b2e6c480", + "bytes": 32339 + }, + { + "path": "images/home/f2a09b5b59ab4dda9f8d87f1.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZWExZmZmNGYtNWVkYy00MzlkLWE2NGYtN2UwYzU0MDE0M2YyXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "f2a09b5b59ab4dda9f8d87f132a71b15e1b5832fdffd00e776be5f6eeea31710", + "bytes": 28993 + }, + { + "path": "images/home/35edb49dabcf6a511e7e044a.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMmE0YTA3MTItMjhiNC00MGQ3LTllZDktNGNiZWY1OWFmNmM1XkEyXkFqcGc@._V1_QL75_UY414_CR26,0,280,414_.jpg", + "sha256": "35edb49dabcf6a511e7e044a07b4089b96245b6ccef7a954f2045429a8e3ff9b", + "bytes": 15463 + }, + { + "path": "images/home/2f785ea7ee5950ce5b0df00a.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BM2ZlYTU0YWUtMzY4OC00N2JkLTk1ZGQtN2UxNWJhMTBkMzM4XkEyXkFqcGc@._V1_QL75_UY414_CR26,0,280,414_.jpg", + "sha256": "2f785ea7ee5950ce5b0df00ac41d93591e8613974879527c3d6fcca58342b5c8", + "bytes": 21368 + }, + { + "path": "images/home/3cbd7e205cda6565c874f2ff.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZDgwMTAzZDUtY2VmNC00MGM2LThiNmQtZDU5N2JlZDQ4ODY0XkEyXkFqcGc@._V1_QL75_UY414_CR26,0,280,414_.jpg", + "sha256": "3cbd7e205cda6565c874f2ff66a05e6d731c8a1d667c6fb46ff455373720018e", + "bytes": 26124 + }, + { + "path": "images/home/3776444ca0b01def1f3bb6fa.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZWI4OTVkNDgtOTBmMC00M2I3LTk3NTctOWY3NmRkODIxMDViXkEyXkFqcGc@._V1_QL75_UY414_CR6,0,280,414_.jpg", + "sha256": "3776444ca0b01def1f3bb6fafe9f3bb1a0c62155f1c45574b32f01cc3ad57a47", + "bytes": 23707 + }, + { + "path": "images/home/890f2366502b614219d624f1.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYjIxNmQ4MDctODQ4Zi00NmYwLTkzNWMtNzEwMmI0ZGFjYmUwXkEyXkFqcGc@._V1_QL75_UY414_CR16,0,280,414_.jpg", + "sha256": "890f2366502b614219d624f195b2adde4f956c267c0e86dd5d40e24b9ed661be", + "bytes": 12049 + }, + { + "path": "images/home/70330856c784831dd856029c.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZGFmMGIxMmItNGYyMi00MzJkLWE5NTgtM2Y3YjMyMjk3YjJjXkEyXkFqcGc@._V1_QL75_UY414_CR16,0,280,414_.jpg", + "sha256": "70330856c784831dd856029c316fe320ff36ad1e995a6d76be73e5e96054a878", + "bytes": 25944 + }, + { + "path": "images/home/2f8b9184632bf5c5f8b22069.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNmI3MjQzN2YtZjMxYy00MWJlLWFlNjYtOTZmY2NkZGE0NTMzXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "2f8b9184632bf5c5f8b22069d9564d4563dce822834fd9033b31ab41458d1e35", + "bytes": 17655 + }, + { + "path": "images/home/bbf0292b90c9fd458eb23abc.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZTMxNGQwM2UtYTFiMS00NzM1LTk3NWQtZmVmYjk3Y2NhODgwXkEyXkFqcGc@._V1_QL75_UY414_CR26,0,280,414_.jpg", + "sha256": "bbf0292b90c9fd458eb23abc34eb470ac2545c0b73da199a03b0476d24fac32a", + "bytes": 16017 + }, + { + "path": "images/home/e1b10aabdb6efa196180c627.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BOWY1Y2ZlNzctMzIwMi00NTM3LWJiNDUtMTZmYWY0Y2NmZmE2XkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "sha256": "e1b10aabdb6efa196180c627e5c216313f6fb1870080cdde796f41b72f76d595", + "bytes": 22700 + }, + { + "path": "images/home/e973e83155c6fa3ab1bb3e84.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNmRmODg5ZTctMTMyMS00YmZmLWEyZTMtMjFmOGU1NjYwMGI2XkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "sha256": "e973e83155c6fa3ab1bb3e8446d8d8a78eac36aa6ec6b3b510634be385c26e30", + "bytes": 40280 + }, + { + "path": "images/home/06133dd3a3207671d9d843c2.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYjE2YzA2YzAtZmE5MS00YmVkLTgxNWItYWYxNmMxZGE5M2Q3XkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "sha256": "06133dd3a3207671d9d843c24f0e5bc92966d8ce06e59725cab31b90b46921ca", + "bytes": 18393 + }, + { + "path": "images/home/d08c167ba897b1e9f57e456b.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNjA0OWVkZGItOTcyZS00MmVmLWI4ODAtMWFjODIxYjE4MDgzXkEyXkFqcGc@._V1_QL75_UY414_CR26,0,280,414_.jpg", + "sha256": "d08c167ba897b1e9f57e456b29a91b4169088a7ca4b61168ef2bb6d7a79ccb07", + "bytes": 22046 + }, + { + "path": "images/home/c3350625ee1aae08877cdaf8.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMmM2NDhmYjktNTIzZC00YjIwLWE1ZmUtNzM1MTNiMTU2NjdjXkEyXkFqcGc@._V1_QL75_UY414_CR26,0,280,414_.jpg", + "sha256": "c3350625ee1aae08877cdaf85d505786ad34fa9f114faf62323da467bfbc0ee7", + "bytes": 17029 + }, + { + "path": "images/home/e5e4eb42d7e7a722291de16b.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMzg1NzAzMWEtMGY2Ny00YzRkLTg4MmEtMDEyZWI2ZjFiMWU5XkEyXkFqcGc@._V1_QL75_UY414_CR26,0,280,414_.jpg", + "sha256": "e5e4eb42d7e7a722291de16b9273477fac4a5c35de5bc5ca55be7073ec54ddb1", + "bytes": 21205 + }, + { + "path": "images/home/be9b716a7e93da8abb4f7079.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNTBlMzA3ZTUtODZjNi00NTM0LWExMjMtNjJhYzA3YTkwMWYwXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "be9b716a7e93da8abb4f7079aba2f8c6ffb400f254b4e56c592d181b5dd1539b", + "bytes": 14864 + }, + { + "path": "images/home/a17ca180acba77c4fa246dbb.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNTVmMWU0ODQtMGZkMi00NzlmLWEwZWYtYmJkZDg2OTUyZTlhXkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "sha256": "a17ca180acba77c4fa246dbb457fef0f5159c32cb17864041f57fe9841e3c6f8", + "bytes": 10872 + }, + { + "path": "images/home/87e2ec2a8f4a2c9e43065688.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZDc4MmYxNTAtM2ViYy00YWQ4LThjZWUtNDZjNTZjNDFhNzBjXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "sha256": "87e2ec2a8f4a2c9e430656880d674ec863e916cd00c9ec34592633e759340684", + "bytes": 21039 + }, + { + "path": "images/home/376b315b3131cf48115e63a2.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BOWJhNTY2NzYtZDk2Yy00OTQzLWExZDUtNTYzMGNmNzZjNjkwXkEyXkFqcGc@._V1_QL75_UY414_CR16,0,280,414_.jpg", + "sha256": "376b315b3131cf48115e63a2798982273b3d96bd24056e32a7f2ce0d62502009", + "bytes": 21815 + }, + { + "path": "images/home/f48f3dde36a4fe75c03c3a0c.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNTM3NTZjMDctYWQ3YS00NjQ4LWIwMjItNmIwZTlkM2U4NjRkXkEyXkFqcGc@._V1_QL75_UY414_CR26,0,280,414_.jpg", + "sha256": "f48f3dde36a4fe75c03c3a0c29c91200c81490263f63fb720b9eac70e17dc195", + "bytes": 30230 + }, + { + "path": "images/home/0a5048a1a4a6f655fdcc6125.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTM2NDkxMTcxMl5BMl5BanBnXkFtZTcwNTMyNjI5MQ@@._V1_QL75_UX280_CR0,7,280,280_.jpg", + "sha256": "0a5048a1a4a6f655fdcc61250ccb66302248944ea7a1dacdd477e2e0374c8071", + "bytes": 9451 + }, + { + "path": "images/home/70771bbbf823b7909b000019.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTUxMDMwMTI4MF5BMl5BanBnXkFtZTcwMzE2MzQxMw@@._V1_QL75_UX280_CR0,9,280,280_.jpg", + "sha256": "70771bbbf823b7909b0000196a3754447d8edc3df86a16a2397206f91f0114ba", + "bytes": 7244 + }, + { + "path": "images/home/fbb22d626a191f61e9a6b828.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTk0MzEwMzEzNV5BMl5BanBnXkFtZTgwNDMzMTc0NDE@._V1_QL75_UX280_CR0,13,280,280_.jpg", + "sha256": "fbb22d626a191f61e9a6b82899726a9380c33ca062e06cabf9a5fbad9a2f9d7c", + "bytes": 12714 + }, + { + "path": "images/home/6e03a42196ac22049942d100.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTcyNjc3MzQwOV5BMl5BanBnXkFtZTYwNTQ0NTc3._V1_QL75_UX280_CR0,25,280,280_.jpg", + "sha256": "6e03a42196ac22049942d100e6f0ce4bfb00984282c5e15e2a7028a9b41242aa", + "bytes": 11676 + }, + { + "path": "images/home/335e41b6462cc11a6a7d1f48.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTY2MTYzNzUyNl5BMl5BanBnXkFtZTYwMDI0NzA0._V1_QL75_UX280_CR0,17,280,280_.jpg", + "sha256": "335e41b6462cc11a6a7d1f4840373e6def2a5901e19749dcd1eb9c4c1ce89c4f", + "bytes": 9798 + }, + { + "path": "images/home/d035a90a7a6666f2b3f78da7.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTY3ODMxMzg4OV5BMl5BanBnXkFtZTcwMDA3NjA2NQ@@._V1_QL75_UX280_CR0,15,280,280_.jpg", + "sha256": "d035a90a7a6666f2b3f78da7508e5159984d60a2aa0209fd55be16bb186ef1a6", + "bytes": 11003 + }, + { + "path": "images/home/c19f0d616e2c2c694d1e0f5c.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTI5ODEyOTQyN15BMl5BanBnXkFtZTYwNzY2NzI1._V1_QL75_UX280_CR0,0,280,280_.jpg", + "sha256": "c19f0d616e2c2c694d1e0f5c5c6ab123b365c40c214bafe47de943babc8ea0e8", + "bytes": 6966 + }, + { + "path": "images/home/3884d19926af0bb2db9a3c6f.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjI1Mzc2MDcyOV5BMl5BanBnXkFtZTgwOTA2NDQ1MjE@._V1_QL75_UX280_CR0,2,280,280_.jpg", + "sha256": "3884d19926af0bb2db9a3c6f8a71264c7756cf6f68535d26b81965caf91f646b", + "bytes": 9420 + }, + { + "path": "images/home/1819ae680be1bbdf0be6bf4c.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMWZmZDNmNDItMGE2My00OTlhLWFjNzAtOGIxMTJhYjZhYWUzXkEyXkFqcGc@._V1_QL75_UX280_CR0,24,280,280_.jpg", + "sha256": "1819ae680be1bbdf0be6bf4ce5f716c5308479f7a44774822f37299b5d753bba", + "bytes": 8545 + }, + { + "path": "images/home/8234e0b8035463951e7c61ef.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BODhkZjg0NWQtOGU3OC00YzIyLTlmMGQtZmFlODc0ZmEyYTYzXkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,280_.jpg", + "sha256": "8234e0b8035463951e7c61ef1a99ce4de94d9fde8ef4ded71e6c0fc3c78beb71", + "bytes": 16119 + }, + { + "path": "images/home/7fde589ea369fa8cf3ffef4f.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjExMDE1MTMzMV5BMl5BanBnXkFtZTYwMDQ2NDIz._V1_QL75_UX280_CR0,25,280,280_.jpg", + "sha256": "7fde589ea369fa8cf3ffef4fbcd99f8bcd6d727e96633d027ccf3dd93f5d71da", + "bytes": 10037 + }, + { + "path": "images/home/ddacaad184dd10b7830d4d2f.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTM5NTA2NDA2MF5BMl5BanBnXkFtZTcwMzY5ODAxMw@@._V1_QL75_UX280_CR0,14,280,280_.jpg", + "sha256": "ddacaad184dd10b7830d4d2fcdb257d6f32fbfb1265cf62f9234fc8a69164679", + "bytes": 9128 + }, + { + "path": "images/home/1e677ebb6b32a590fa87bf01.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTgzOTgxNDQ5N15BMl5BanBnXkFtZTcwMzUwOTcyMQ@@._V1_QL75_UX280_CR0,0,280,280_.jpg", + "sha256": "1e677ebb6b32a590fa87bf016fe8d09bd70b5b1f6e6b01578aed10cd975c8b04", + "bytes": 11529 + }, + { + "path": "images/home/0e1a519601d6a72b9a551095.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BOTRjMjJmMmYtMDJhZi00ZWU2LTgxZWUtMmVhZDI1ZmM5ZTM5XkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,280_.jpg", + "sha256": "0e1a519601d6a72b9a551095bec024f4d65b25563985bffa88422b4556caa3c7", + "bytes": 7790 + }, + { + "path": "images/home/bdf615e7b10d39c0ffeebba2.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNjYzNTExMmItM2NlYi00ZWU4LWJmNGQtNTIyY2FkYzczYWJlXkEyXkFqcGc@._V1_QL75_UX280_CR0,24,280,280_.jpg", + "sha256": "bdf615e7b10d39c0ffeebba250b91c189f879ae8ef3679d92ac6396a3d112cda", + "bytes": 10424 + }, + { + "path": "images/home/22f075e240c756aee434f304.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTMyNjMxMjgwMF5BMl5BanBnXkFtZTcwNzE0MzQ3Mg@@._V1_QL75_UX280_CR0,30,280,280_.jpg", + "sha256": "22f075e240c756aee434f30466e65a6d8afee8b08102fe3acfd3bf31c916f36a", + "bytes": 11816 + }, + { + "path": "images/home/55c45a2bd384be788feef002.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjExOTEyNDcwMl5BMl5BanBnXkFtZTgwMjM3NzM0OTE@._V1_QL75_UX280_CR0,24,280,280_.jpg", + "sha256": "55c45a2bd384be788feef002a4fc19ea7c8a62911ee458774811d8d04be13e11", + "bytes": 9172 + }, + { + "path": "images/home/ec5f738ef1b39e3edf23e77a.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMmNlODI4OTMtNDgxNy00MGZjLTgyYWItOTQyMmY5NTc3YWM0XkEyXkFqcGc@._V1_QL75_UX280_CR0,24,280,280_.jpg", + "sha256": "ec5f738ef1b39e3edf23e77ae16a651bd68c3fc2ef3178fbae6d671e100293fb", + "bytes": 8764 + }, + { + "path": "images/home/5115f0645760f50d3030a058.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjAxNDczNjc5Ml5BMl5BanBnXkFtZTgwMjM3MTI1NTE@._V1_QL75_UX280_CR0,22,280,280_.jpg", + "sha256": "5115f0645760f50d3030a05844e7a955ae42c8760024f6a0806c42c98320f012", + "bytes": 10111 + }, + { + "path": "images/home/9833654f668c9fd799fc64dd.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNjcyMGZhN2ItODA1NS00ZWE3LTk1MzYtMWVjYjA2OTA1NzAxXkEyXkFqcGc@._V1_QL75_UY280_CR58,0,280,280_.jpg", + "sha256": "9833654f668c9fd799fc64dda06500eaa7cef87654ccd1cc3e1ed5f28e22e90d", + "bytes": 15779 + }, + { + "path": "images/home/a90325c70b455cf5eec78ee2.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYTQ0M2EzNDktMTEwZC00ODZjLWIyNDctYWMyYzQ4NTZjMmZlXkEyXkFqcGc@._V1_QL75_UY280_CR70,0,280,280_.jpg", + "sha256": "a90325c70b455cf5eec78ee271c8a36d09080e94cdb5bef91e388b624d87215b", + "bytes": 6066 + }, + { + "path": "images/home/18bc6a003e16aa7afe377b9e.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BODI3MjQ0OTMwMl5BMl5BanBnXkFtZTYwNDE4NDky._V1_QL75_UX280_CR0,0,280,280_.jpg", + "sha256": "18bc6a003e16aa7afe377b9e8a5390e148e37affdbd7402ebb4c3e1e43347b57", + "bytes": 11578 + }, + { + "path": "images/home/efebbf80f594b7e190644b6c.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjMxMTM3NzY0Ml5BMl5BanBnXkFtZTgwMTI5MjQ1MzE@._V1_QL75_UX280_CR0,25,280,280_.jpg", + "sha256": "efebbf80f594b7e190644b6c2933307bd7fe86c273a727bb4b14318b51543755", + "bytes": 8963 + }, + { + "path": "images/home/435dade49bd62c944765193c.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjExODU1NTc4OF5BMl5BanBnXkFtZTcwMjg1NjgyNA@@._V1_QL75_UX280_CR0,25,280,280_.jpg", + "sha256": "435dade49bd62c944765193c4c45e505f40064c3a00ad4237fa8d5ea546b9abf", + "bytes": 12245 + }, + { + "path": "images/home/5a5cf817259e764723780201.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTA5MDYxNTc4OTVeQTJeQWpwZ15BbWU2MDg0MDYwNQ@@._V1_QL75_UX280_CR0,1,280,280_.jpg", + "sha256": "5a5cf817259e7647237802011b20ead734d6109f8057e0bd8a56e6863757dfb8", + "bytes": 8930 + }, + { + "path": "images/home/fcab9c6688bbc9170aba1e12.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BOTk2Mzg5MjgwN15BMl5BanBnXkFtZTgwNDk4Mzc5NzE@._V1_QL75_UY280_CR70,0,280,280_.jpg", + "sha256": "fcab9c6688bbc9170aba1e12246f1e6c64893afa86dbcf39de46d11e4ccba493", + "bytes": 13599 + }, + { + "path": "images/home/e494f5d4427b0747361052bd.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTU4OTE3OTg3MF5BMl5BanBnXkFtZTYwMzU4Mzcz._V1_QL75_UX280_CR0,26,280,280_.jpg", + "sha256": "e494f5d4427b0747361052bdc972332b9e44da154230ff033e94da7c8ac2e74f", + "bytes": 13008 + }, + { + "path": "images/home/fdb8058a8fdeb3ab9c19d1ef.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTgxODY1MTcyMV5BMl5BanBnXkFtZTYwODAwNTA1._V1_QL75_UX280_CR0,30,280,280_.jpg", + "sha256": "fdb8058a8fdeb3ab9c19d1efe0e55e1edd3d1c5817bd4f0da107f7ab236867ba", + "bytes": 7922 + }, + { + "path": "images/home/c1a2bbd20406319eb2d3f689.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTY4MDAxNzUzMl5BMl5BanBnXkFtZTgwMDk5MzM0MDE@._V1_QL75_UX280_CR0,25,280,280_.jpg", + "sha256": "c1a2bbd20406319eb2d3f6890440583911e3c9ecb625fddf6c48f17a23310365", + "bytes": 10421 + }, + { + "path": "images/home/0cccbbce9c4f8f4171da0a1c.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTQxMjAzNDg4Ml5BMl5BanBnXkFtZTcwNTc4NjQyOA@@._V1_QL75_UX280_CR0,17,280,280_.jpg", + "sha256": "0cccbbce9c4f8f4171da0a1cd979ebf20752a0b08f83a6547ca88314ecf93826", + "bytes": 10433 + } + ], + "protected_tables_unchanged": [ + "users", + "titles", + "persons", + "genres", + "news_items", + "title_genre", + "credits", + "reviews", + "user_ratings", + "watchlist_items" + ], + "seed_sha256": "e3219e9c76eadc9c0a203d6d0ae55ec951ed3837e69dbd3be08697186be0d94f" +} diff --git a/sites/imdb/docs/most-anticipated-2026-09-12.json b/sites/imdb/docs/most-anticipated-2026-09-12.json new file mode 100644 index 00000000..3accc844 --- /dev/null +++ b/sites/imdb/docs/most-anticipated-2026-09-12.json @@ -0,0 +1,293 @@ +{ + "feature_id": "featured-today-1", + "source_url": "https://www.imdb.com/most-anticipated/this-month/", + "observed_at": "2026-09-12T00:38:02+08:00", + "card_title": "What to Watch in September", + "card_subtitle": "See the most anticipated movies and shows coming in September.", + "page_title": "Here's what to watch in September", + "page_subtitle": "Don't find yourself with an empty Watchlist this month. Here are some of the biggest movies and shows people will be talking about in September.", + "hero_image_path": "images/home/0507488fe37aadd96fc8d7bc.jpg", + "hero_image_sha256": "0507488fe37aadd96fc8d7bc3d292472d02f7c277d81e59876833a2bc5ac7982", + "hero_image_source_url": "https://m.media-amazon.com/images/M/MV5BODY3Y2MxNDktY2UxNS00OTEyLWI0MGUtNDY4YjQ0OGRmZWE1XkEyXkFqcGc@._V1_QL75_UX4096_.jpg", + "items": [ + { + "position": 1, + "source_id": "tt27369017", + "title": "Hopeu", + "source_url": "https://www.imdb.com/title/tt27369017/", + "poster_path": "images/home/cdcd224e1dd55ac61263c818.jpg", + "poster_sha256": "cdcd224e1dd55ac61263c818286d780fe525f76717f25b726e94aab03247381a", + "poster_source_url": "https://m.media-amazon.com/images/M/MV5BZjE3ZDI0MmItMDIyMC00NzU2LTk2OTQtZTUxYTlkMjlkNmY3XkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "poster_alt": "Hoyeon in Hopeu (2026)", + "rating": "6.9", + "votes": "2.9K", + "popularity": "#32 MOVIEmeter", + "metadata": ["Movie", "2h 40m", "R"], + "release_context": "In theaters September 9", + "description": "Beloved writer-director Na Hong-jin returns ten years after The Wailing with a monster movie that premiered at Cannes, where it divided audiences, racked up distribution deals around the world, and made further news by providing the mostly civilized festival with nonstop alien mayhem. Hwang Jung-min (The Wailing) and Hoyeon (\"Squid Game\") co-star as police officers who are dispatched to the brewing chaos happening in a remote South Korean village. Sit tight for head turning appearances from Hollywood stars (and real-life couple) Alicia Vikander and Michael Fassbender. For big-big-picture people, distributor NEON scored a two-day large format release in the States starting September 7. -- Arno", + "featuring": ["Hwang Jung-min", "Hoyeon", "Zo In-sung", "Alicia Vikander", "Michael Fassbender", "Taylor Russell"] + }, + { + "position": 2, + "source_id": "tt32588798", + "title": "Practical Magic 2", + "source_url": "https://www.imdb.com/title/tt32588798/", + "poster_path": "images/home/7a165e0d76d23af460515794.jpg", + "poster_sha256": "7a165e0d76d23af460515794dbbab3d0d7977410cf1568d4e82aab208fbb31f4", + "poster_source_url": "https://m.media-amazon.com/images/M/MV5BMWUyY2UyNjgtYTg5MC00MDZmLWE1MzEtZmU2YWE2ZTMzMjUwXkEyXkFqcGc@._V1_QL75_UY414_CR26,0,280,414_.jpg", + "poster_alt": "Sandra Bullock, Nicole Kidman, Joey King, and Maisie Williams in Practical Magic 2 (2026)", + "rating": "6.4", + "votes": "812", + "popularity": "#19 MOVIEmeter", + "metadata": ["Movie", "2h 10m", "PG-13"], + "release_context": "In theaters September 11", + "description": "My blood. Your blood. Our blood. Sequel blood. Thank you to the spirit world for sending Practical Magic 2 to us, and for making it feel like no time has passed over the last 28 years. Good to see Sandra Bullock, Nicole Kidman, Stockard Channing (both my king and queen), and Dianne Wiest are still calling the corners together. In this story of family bonds, cozy New England drama, and the seductive embrace of dark magic, the youngest generation of the Owen's family -- Sally's kids, Kylie and Antonia -- will help the coven hopefully break the curse that forbids any of them from having true love. Even though Practical Magic created its own subgenre, right now I'm into the entire The Craft / \"Widow's Bay\" feeling the sequel is emanating. -- Arno", + "featuring": ["Sandra Bullock", "Nicole Kidman", "Stockard Channing", "Dianne Wiest"] + }, + { + "position": 3, + "source_id": "tt36983905", + "title": "The Uprising", + "source_url": "https://www.imdb.com/title/tt36983905/", + "poster_path": "images/home/e4cd12048a26023562b8ba6e.jpg", + "poster_sha256": "e4cd12048a26023562b8ba6e07d9b7cda51647646d38c71dd024785e1982c4b2", + "poster_source_url": "https://m.media-amazon.com/images/M/MV5BYmNiZjVhOGItZTUxMi00MDUwLWJlNDItYTg2MTViZmQzMmU0XkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "poster_alt": "Andrew Garfield in The Uprising (2026)", + "rating": "6.5", + "votes": "348", + "popularity": "#49 MOVIEmeter", + "metadata": ["Movie", "2h 7m", "R"], + "release_context": "In theaters September 11", + "description": "Director Paul Greengrass' retelling of the English Peasants' Revolt of 1381 pits a mad-as-hell Andrew Garfield against the tyranny of King Richard II. The Uprising sits firmly in Greengrass' wheelhouse, mixing the gritty, shaky-cam action of his Jason Bourne films, as well as his passion for historical dramas, especially stories about regular people collectively fighting for their survival (Bloody Sunday, United 93). -- James", + "featuring": ["Andrew Garfield", "Jamie Bell", "Thomasin McKenzie", "Woody Norman"] + }, + { + "position": 4, + "source_id": "tt26545992", + "title": "Lanterns", + "source_url": "https://www.imdb.com/title/tt26545992/", + "poster_path": "images/home/f5671c08bcae4c546226e2f6.jpg", + "poster_sha256": "f5671c08bcae4c546226e2f6efe267e188e591af05dc3ef01c733e2af5c05dd6", + "poster_source_url": "https://m.media-amazon.com/images/M/MV5BM2ZjZDNlNDgtMjJlMy00NGRjLThlMGEtM2U4N2E4MDUwNjNmXkEyXkFqcGc@._V1_QL75_UY414_CR20,0,280,414_.jpg", + "poster_alt": "Kyle Chandler and Aaron Pierre in Lanterns (2026)", + "rating": "7.8", + "votes": "29K", + "popularity": "#3 TVmeter", + "metadata": ["TV Series", "2 seasons", "TV-MA"], + "release_context": "Streaming on HBO Max", + "description": "Kyle Chandler (\"Friday Night Lights\") and Aaron Pierre (Rebel Ridge) join the DC Universe as two members of the intergalactic peacekeeping group, the Green Lantern Corps. In what looks to be a very grounded premise for the HBO series, Hal Jordan (Chandler) takes new recruit John Stewart (Pierre) under his wing as the pair investigate a murder in Nebraska. Fans of the \"Watchmen\" limited series will be interested to know that \"Lanterns\" has a shared co-creator in Damon Lindelof (\"Lost,\" \"The Leftovers\"), who worked wonders with that source material. Crossing our ring fingers that he can do the same for the DCU. -- James", + "featuring": ["Aaron Pierre", "Kyle Chandler", "Nathan Fillion", "Laura Linney"] + }, + { + "position": 5, + "source_id": "tt13210838", + "title": "The Gentlemen", + "source_url": "https://www.imdb.com/title/tt13210838/", + "poster_path": "images/home/d41828e59321ece02267063c.jpg", + "poster_sha256": "d41828e59321ece02267063c6c87b30ede0fbc5b29d10ac6ad2b4aa07ed3c148", + "poster_source_url": "https://m.media-amazon.com/images/M/MV5BMzhhZDFhOGYtZjJjOC00MDlhLThiMmEtYzFhYWYzMWQyNTBkXkEyXkFqcGc@._V1_QL75_UY414_CR8,0,280,414_.jpg", + "poster_alt": "The Gentlemen (2024)", + "rating": "8.0", + "votes": "190K", + "popularity": "#1 TVmeter", + "metadata": ["TV Series", "3 seasons", "TV-MA"], + "release_context": "Season 2 streaming on Netflix", + "description": "Guy Ritchie's hit action-comedy series, which sports an 8.0 IMDb rating, returns to Netflix for Season 2. The new season finds Eddie Horniman (Theo James) and Susie Glass (Kaya Scodelario) embracing Britain's criminal underground after their hostile takeover of Susie's now-imprisoned dad's weed empire. The show is pulling an \"Emily in Paris\" of sorts by shifting some of the action to Italy for a spell. -- Arno", + "featuring": ["Theo James", "Kaya Scodelario", "Ray Winstone", "Giancarlo Esposito", "Daniel Ings"] + }, + { + "position": 6, + "source_id": "tt28014327", + "title": "Mayday", + "source_url": "https://www.imdb.com/title/tt28014327/", + "poster_path": "images/home/7c721c48f2001af06fbb2f62.jpg", + "poster_sha256": "7c721c48f2001af06fbb2f62f751698ba2de44aa039b505a308616a41334bcc4", + "poster_source_url": "https://m.media-amazon.com/images/M/MV5BM2VmYWEzM2UtODNjZi00YTk1LWEzZjEtNzY4OGU0YTdkNmJmXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "poster_alt": "Kenneth Branagh and Ryan Reynolds in Mayday (2026)", + "rating": "6.8", + "votes": "21K", + "popularity": "#1 MOVIEmeter", + "metadata": ["Movie", "1h 50m", "PG-13"], + "release_context": "Streaming on Apple TV+", + "description": "What decade is it? Because here come Ryan Reynolds and Sir Kenneth Branagh with a Cold War-era action-comedy movie set inside the late '80s Soviet Union. You will guess Reynolds's role correctly: He plays a military operative (a U.S. Navy pilot) who gets stranded behind enemy lines. Branagh gets the Jackman role as a gruff former KGB agent with a secret fetish for American culture. Okay, I am staying home yet again on a Friday night (Apple TV tends to do this to me with their original movies). The nice guys who co-directed Dungeons & Dragons: Honor Among Thieves are behind this survival story. -- Arno", + "featuring": ["Ryan Reynolds", "Kenneth Branagh"] + }, + { + "position": 7, + "source_id": "tt5875444", + "title": "Slow Horses", + "source_url": "https://www.imdb.com/title/tt5875444/", + "poster_path": "images/home/c3350625ee1aae08877cdaf8.jpg", + "poster_sha256": "c3350625ee1aae08877cdaf85d505786ad34fa9f114faf62323da467bfbc0ee7", + "poster_source_url": "https://m.media-amazon.com/images/M/MV5BMmM2NDhmYjktNTIzZC00YjIwLWE1ZmUtNzM1MTNiMTU2NjdjXkEyXkFqcGc@._V1_QL75_UY414_CR26,0,280,414_.jpg", + "poster_alt": "Gary Oldman and Jack Lowden in Slow Horses (2022)", + "rating": "8.3", + "votes": "155K", + "popularity": "#21 TVmeter", + "metadata": ["TV Series", "8 seasons", "TV-MA"], + "release_context": "Season 6 premieres September 16 on Apple TV+", + "description": "Season 6 finds Jackson Lamb (Gary Oldman) and the Slough House crew on the run as they're being systematically targeted and turning up dead. While signs point to a connection to MI5 operator Diana Taverner (Kristin Scott Thomas), things get interesting with the surprise return of Sid Baker (Olivia Cooke), the agent believed dead since Season 1. The new season adapts two Mick Herron novels (\"Joe Country\" and \"Slough House\") and the Apple TV hit has already been renewed for a seventh season. -- James", + "featuring": ["Gary Oldman", "Jack Lowden", "Kristin Scott Thomas", "Saskia Reeves", "Olivia Cooke"] + }, + { + "position": 8, + "source_id": "tt33539520", + "title": "Neagley", + "source_url": "https://www.imdb.com/title/tt33539520/", + "poster_path": "images/home/e5e4eb42d7e7a722291de16b.jpg", + "poster_sha256": "e5e4eb42d7e7a722291de16b9273477fac4a5c35de5bc5ca55be7073ec54ddb1", + "poster_source_url": "https://m.media-amazon.com/images/M/MV5BMzg1NzAzMWEtMGY2Ny00YzRkLTg4MmEtMDEyZWI2ZjFiMWU5XkEyXkFqcGc@._V1_QL75_UY414_CR26,0,280,414_.jpg", + "poster_alt": "Maria Sten in Neagley (2026)", + "popularity": "#82 TVmeter", + "metadata": ["TV Series", "1 season"], + "release_context": "Premieres September 16 on Prime Video", + "description": "\"Reacher\" becomes its own universe with this spin-off series centered on the character Frances Neagley, Jack Reacher's former military protégé who now works as a private investigator in Chicago. Neagley, played by Maria Sten, appeared in the first three seasons of \"Reacher\" before her series went into production. Alan Ritchson will guest star in Season 1 of \"Neagley,\" which finds the PI on a hunt for answers and justice after one of her longtime friends is killed in a suspicious accident. -- Arno", + "featuring": ["Maria Sten", "Adeline Rudolph", "Greyston Holt", "Jasper Jones", "Matthew Del Negro", "Alan Ritchson"] + }, + { + "position": 9, + "source_id": "tt13207736", + "title": "Monster", + "source_url": "https://www.imdb.com/title/tt13207736/", + "poster_path": "images/home/a17ca180acba77c4fa246dbb.jpg", + "poster_sha256": "a17ca180acba77c4fa246dbb457fef0f5159c32cb17864041f57fe9841e3c6f8", + "poster_source_url": "https://m.media-amazon.com/images/M/MV5BNTVmMWU0ODQtMGZkMi00NzlmLWEwZWYtYmJkZDg2OTUyZTlhXkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "poster_alt": "Ella Beatty in Monster (2022)", + "rating": "7.7", + "votes": "226K", + "popularity": "#74 TVmeter", + "metadata": ["TV Series", "4 seasons", "(Banned)"], + "release_context": "Premieres September 17 on Netflix", + "description": "We're deep into Ryan Murphy season with \"American Horror Story\" back later this month and \"The Shards\" wrapping up its freshman debut. The fourth installment of Murphy's slayer series will tell the story of accused ax murderer Lizzie Borden. Ella Beatty (\"Feud\") stars as Lizzie, a young woman who stood trial for the murder of her father and stepmother in 1892. In typical \"Monster\" fashion, the show will take a highly editorialized approach, spinning out unproven theories into plot points about everything from family tensions to a homicidal love story between Lizzie and the maid (Vicky Krieps). Charlie Hunnam, who played Ed Gein in \"Monster\" Season 3, returns alongside frequent Murphy collaborator Billie Lourd and Rebecca Hall to round out the doomed Borden brood. -- Adrienne", + "featuring": ["Ella Beatty", "Charlie Hunnam", "Rebecca Hall", "Vicky Krieps", "Billie Lourd"] + }, + { + "position": 10, + "source_id": "tt35538033", + "title": "Resident Evil", + "source_url": "https://www.imdb.com/title/tt35538033/", + "poster_path": "images/home/683fd710ce4eea34cfcc6fdf.jpg", + "poster_sha256": "683fd710ce4eea34cfcc6fdf0eaf9efb5acc208b8d09976d7586efb2bfc92bb8", + "poster_source_url": "https://m.media-amazon.com/images/M/MV5BODIwYTk4MWYtMGUxYS00OWUzLTg5MDYtNzg5MmYyNzcyNThkXkEyXkFqcGc@._V1_QL75_UY414_CR26,0,280,414_.jpg", + "poster_alt": "Resident Evil (2026)", + "popularity": "#29 MOVIEmeter", + "metadata": ["Movie", "1h 30m", "R"], + "release_context": "In theaters September 18", + "description": "Forget everything you know about the Resident Evil film franchise. Director Zach Cregger (the creator of last year's horror breakout Weapons) has been quoted as saying he's never seen any of the previous seven Resident Evil movies, choosing to ground his film in the world of the video game series. After hilariously stealing scenes in Weapons as the drug addict James, Austin Abrams (also in the upcoming Whalefall) gets top billing as a medical courier whose delivery route takes a turn when a viral outbreak turns Raccoon City into a hotbed of zombies. -- James", + "featuring": ["Austin Abrams", "Paul Walter Hauser", "Zach Cherry"] + }, + { + "position": 11, + "source_id": "tt31510819", + "title": "MobLand", + "source_url": "https://www.imdb.com/title/tt31510819/", + "poster_path": "images/home/87e2ec2a8f4a2c9e43065688.jpg", + "poster_sha256": "87e2ec2a8f4a2c9e430656880d674ec863e916cd00c9ec34592633e759340684", + "poster_source_url": "https://m.media-amazon.com/images/M/MV5BZDc4MmYxNTAtM2ViYy00YWQ4LThjZWUtNDZjNTZjNDFhNzBjXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "poster_alt": "Pierce Brosnan, Helen Mirren, and Tom Hardy in MobLand (2025)", + "rating": "8.3", + "votes": "123K", + "popularity": "#25 TVmeter", + "metadata": ["TV Series", "3 seasons", "TV-MA"], + "release_context": "Season 2 premieres September 18 on Paramount+", + "description": "Tom Hardy's mob fixer Harry Da Souza is back for a second season of \"MobLand,\" the massive hit series that set and broke some viewership records during its freshman run on Paramount+. Season 2 will reveal the cracks in the Harrigan family's criminal empire as a civil war breaks out within its own ranks. Season 3 is reportedly in development already, though you might have heard a row between Hardy and showrunner Jez Butterworth found Hardy's participation hanging in the balance for a moment. Ten new episodes will roll out weekly starting September 18. -- Arno", + "featuring": ["Tom Hardy", "Helen Mirren", "Pierce Brosnan", "Janet McTeer", "Anson Boon", "Paddy Considine"] + }, + { + "position": 12, + "source_id": "tt22526100", + "title": "The Love Hypothesis", + "source_url": "https://www.imdb.com/title/tt22526100/", + "poster_path": "images/home/231e1ff4a6c52c843707d683.jpg", + "poster_sha256": "231e1ff4a6c52c843707d683a80b65416a64cd87cc177b82dfd6390184b2f958", + "poster_source_url": "https://m.media-amazon.com/images/M/MV5BYWI2ZWZjZGItZDFiMi00ODBmLThmODktY2FlZmNkNmU4YTEzXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "poster_alt": "Lili Reinhart and Tom Bateman in The Love Hypothesis (2026)", + "popularity": "#158 MOVIEmeter", + "metadata": ["Movie", "R"], + "release_context": "Premieres September 23 on Prime Video", + "description": "Lili Reinhart (\"Riverdale\") and Tom Bateman (\"Behind Her Eyes\") star in this adaptation of Ali Hazelwood's bestselling novel as a PhD candidate and her professor who start a fake relationship in order to test their theories on love. Maybe you can see where this is going, but did you know that Hazelwood's novel originally began as a Star Wars fan fic about Kylo Ren and Rey? -- James", + "featuring": ["Lili Reinhart", "Tom Bateman", "Nicholas Duvernay"] + }, + { + "position": 13, + "source_id": "tt1844624", + "title": "American Horror Story", + "source_url": "https://www.imdb.com/title/tt1844624/", + "poster_path": "images/home/f48f3dde36a4fe75c03c3a0c.jpg", + "poster_sha256": "f48f3dde36a4fe75c03c3a0c29c91200c81490263f63fb720b9eac70e17dc195", + "poster_source_url": "https://m.media-amazon.com/images/M/MV5BNTM3NTZjMDctYWQ3YS00NjQ4LWIwMjItNmIwZTlkM2U4NjRkXkEyXkFqcGc@._V1_QL75_UY414_CR26,0,280,414_.jpg", + "poster_alt": "American Horror Story (2011)", + "rating": "7.9", + "votes": "365K", + "popularity": "#61 TVmeter", + "metadata": ["TV Series", "14 seasons", "TV-MA"], + "release_context": "Season 13 premieres September 24 on FX", + "description": "It doesn't work for everybody, but \"AHS\" fans love Ryan Murphy's everything-but-the-kitchen-sink approach to his longest-running TV series. From Season 1 all the way back in 2011 -- which featured sexy ghosts, haunted neighbors, mad scientists, teenage stalkers, demonic fetuses, and red-headed murder twins -- Murphy and his collaborators haven't been afraid to slash for the fences, so to speak. Expect nothing less from Season 13. While there were rumors the next installment was a \"Coven\" sequel, the producer says it's actually \"all the seasons\" combined. Jessica Lange, Sarah Paulson, Evan Peters, Kathy Bates, and Emma Roberts will all return to reprise their roles as ... everyone, as they each portray multiple characters from the previous seasons. \"It's like the Avengers!\" Murphy told Variety, \"They all come together to defeat the ultimate evil.\" -- Adrienne", + "featuring": ["Jessica Lange", "Sarah Paulson", "Evan Peters", "Kathy Bates"] + }, + { + "position": 14, + "source_id": "tt33081352", + "title": "A Different World", + "source_url": "https://www.imdb.com/title/tt33081352/", + "poster_path": "images/home/169abb8b25cf4212a559f6fc.jpg", + "poster_sha256": "169abb8b25cf4212a559f6fc6556b412b77ba23f702757f052536bdb2de0af72", + "poster_source_url": "https://m.media-amazon.com/images/M/MV5BNGIwZDc3OWYtZTZkOS00NmRjLTk3NTAtNjQzODI3ZTI4MWU4XkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "poster_alt": "Maleah Joi Moon in A Different World (2026)", + "popularity": "#628 TVmeter", + "metadata": ["TV Series", "1 season"], + "release_context": "Premieres September 24 on Netflix", + "description": "Let's re-enroll at Hillman College in the present day, where Deborah Wayne, the daughter of Whitley and Dwayne from the beloved '80s series, arrives on campus for her freshman year. She'll be joined by an ensemble of other first-year coeds, as well as OG \"ADW\" characters Whitley Gilbert (Jasmine Guy), Dwayne Wayne (Kadeem Hardison), Freddie Brooks (Cree Summer), and Ron Johnson (Darryl M. Bell). For some of us, the world will never quite be the same without Lisa Bonet's Denise Huxtable in the mix, though. -- Arno", + "featuring": ["Maleah Joi Moon", "Cornell Young IV", "Kadeem Hardison", "Jasmine Guy"] + }, + { + "position": 15, + "source_id": "tt34206385", + "title": "Primetime", + "source_url": "https://www.imdb.com/title/tt34206385/", + "poster_path": "images/home/4212c9fd830e4d1403226cf8.jpg", + "poster_sha256": "4212c9fd830e4d1403226cf8991c7a69ac6afe1ca4e9ea9e03a2d4d025f6c334", + "poster_source_url": "https://m.media-amazon.com/images/M/MV5BMTcyZGI2NWQtOTU3NS00NTEyLTliNDItNTQyNmFkMGZmZGQzXkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "poster_alt": "Robert Pattinson in Primetime (2026)", + "rating": "7.6", + "votes": "322", + "popularity": "#31 MOVIEmeter", + "metadata": ["Movie", "1h 50m", "R"], + "release_context": "In theaters September 25", + "description": "For Robert Pattinson's next trick, the transformative actor will step into the shoes of Chris Hansen, TV journalist and host of \"To Catch a Predator.\" The rise and fall of Hansen's hidden camera show, which lured potential predators to meet up with minors before arresting them, was an early 21st century phenomenon. But all viral hits come to an end, and by 2008 the series was canceled in the wake of horrifying events that will likely make up the climax of the movie. Expect a crime thriller in the style of Nightcrawler, and don't skip out on watching the trailer for this one, which is a work of art in itself. -- Adrienne", + "featuring": ["Robert Pattinson", "Anna Faris", "Skyler Gisondo", "Merritt Wever"] + }, + { + "position": 16, + "source_id": "tt6933238", + "title": "Unabomber", + "source_url": "https://www.imdb.com/title/tt6933238/", + "poster_path": "images/home/2f5e7b638a9700aa035eefd7.jpg", + "poster_sha256": "2f5e7b638a9700aa035eefd748fe6a3f6b3e1504ef2d9351bc1dd8f5b8b02390", + "poster_source_url": "https://m.media-amazon.com/images/M/MV5BMDE2ODhlNTUtYjdiOS00OTlmLTg5OGMtMGNjZWZmNWE4MGJiXkEyXkFqcGc@._V1_QL75_UX280_CR0,0,280,414_.jpg", + "poster_alt": "Jacob Tremblay in Unabomber (2026)", + "popularity": "#193 MOVIEmeter", + "metadata": ["Movie", "1h 37m", "R"], + "release_context": "Premieres September 25 on Netflix", + "description": "Netflix continues to expand its reach into true crime with a new film about Ted Kaczynski, aka the Unabomber. Jacob Tremblay stars as Kaczynski in dual timelines that reveal the young math prodigy's journey from Harvard undergrad to test subject to domestic terrorist. Supporting Tremblay is Shailene Woodley as FBI agent Joanne Miller and Russell Crowe as Henry Murray, a Harvard professor who used Kaczynski in a series of unethical psychological experiments. -- Adrienne", + "featuring": ["Jacob Tremblay", "Russell Crowe", "Shailene Woodley", "Annabelle Wallis"] + }, + { + "position": 17, + "source_id": "tt34374553", + "title": "Victorian Psycho", + "source_url": "https://www.imdb.com/title/tt34374553/", + "poster_path": "images/home/32fce96a5fd819965827ac58.jpg", + "poster_sha256": "32fce96a5fd819965827ac58059408ae4d73daf7ef07c1dcf5ac5bfd091c3a6c", + "poster_source_url": "https://m.media-amazon.com/images/M/MV5BNjdmNmYwMzEtMzJmYS00MDkxLThiNGItZjA0MGJlMjU4MTNmXkEyXkFqcGc@._V1_QL75_UX280_CR0,3,280,414_.jpg", + "poster_alt": "Maika Monroe in Victorian Psycho (2026)", + "rating": "5.1", + "votes": "296", + "popularity": "#896 MOVIEmeter", + "metadata": ["Movie", "1h 42m", "R"], + "release_context": "In theaters September 25", + "description": "Resist the urge to make American Psycho comparisons with this new horror comedy from Zachary Wigon (Sanctuary), which is likely to bring a few more belly laughs along with its mayhem. Based on screenwriter Virginia Feito's novel of the same name, Victorian Psycho follows the possibly (definitely) insane (homicidal?) governess Winifred Notty (Maika Monroe) as she brings her unique set of skills to service the stuffy inhabitants of a remote gothic manor. Is all hell about to break loose? Yes. Are you going to have a great time watching it happen? Also yes. Will there be blood? As the Victorians might say, \"Most assuredly.\" -- Adrienne", + "featuring": ["Thomasin McKenzie", "Maika Monroe", "Ruth Wilson", "Jason Isaacs"] + } + ] +} diff --git a/sites/imdb/docs/most-anticipated-import.json b/sites/imdb/docs/most-anticipated-import.json new file mode 100644 index 00000000..da9f2feb --- /dev/null +++ b/sites/imdb/docs/most-anticipated-import.json @@ -0,0 +1,19 @@ +{ + "snapshot_sha256": "46f2a02ab59fb57de67f17f101177b2ba51930fe42ac1073b384aec71843d019", + "observed_at": "2026-09-12T00:38:02+08:00", + "feature_id": "featured-today-1", + "items": 17, + "protected_tables_unchanged": [ + "credits", + "genres", + "news_items", + "persons", + "reviews", + "title_genre", + "titles", + "user_ratings", + "users", + "watchlist_items" + ], + "seed_sha256": "9d843c5388ecbc0d5265ac316cdaee915f8b203121b5a5330d207af5fb6206b0" +} diff --git a/sites/imdb/docs/seed-provenance.md b/sites/imdb/docs/seed-provenance.md new file mode 100644 index 00000000..bcc13e62 --- /dev/null +++ b/sites/imdb/docs/seed-provenance.md @@ -0,0 +1,128 @@ +# IMDb seed corrections + +The original seed attached some other people's profile data to the requested +IMDb `nm_id`. For example, `nm1165110` was labelled Robin Williams even though +[IMDb identifies that exact ID as Chris Hemsworth playing Thor in Endgame](https://www.imdb.com/title/tt4154796/characters/nm1165110/). +The loader now requires a matching canonical Person URL before importing a +scraped profile. Missing, malformed and mismatching identities are skipped. +The existing title canonical-URL check remains in place. + +The contributor's title catalog, credit relationships, image files and user +state are retained, with two sourced title-year corrections described below. +`migrate_seed.py` is an offline asset migration; it is not called during app +startup or reset. + +## Sources and scope + +The input is `imdb/instance_seed/imdb.db` from `imdb.tar.gz` in the +[original HF asset revision](https://huggingface.co/datasets/ChilleD/WebHarbor/tree/51c523d1c02a57caaaea44c76f3f96646d15bcea): + +- Revision: `51c523d1c02a57caaaea44c76f3f96646d15bcea` +- Archive SHA256: `333800bc9919c6a7f258e8e49cca611929886f6dfe222d02773006b5a5368f75` +- Input seed SHA256: `f9f1431142d528c3126663a452dc12df0a8cd952f49b0b0be5fb807fd8bb5abf` + +The replacement facts come from [IMDb's official non-commercial datasets](https://data.imdb.com/non-commercial-datasets/), +specifically `name.basics.tsv.gz`. Each correction uses an exact `nconst`, +never a fuzzy name match. `seed_corrections.json` contains only the selected +official facts, their source metadata, and correction classifications. + +- Retrieval completed: `2026-09-07T17:07:30.738877+00:00` +- Actual GET Last-Modified: `Mon, 07 Sep 2026 00:32:01 GMT` +- Source run date: `2026-09-06` +- Compressed bytes: `309376069` +- Complete compressed-stream SHA256: `8381884fe8796b09d1f6f6f2871ab6ccdfc7dfbc1a10fada14d45a3f98307aea` + +The full gzip was consumed to EOF and its CRC and declared length checked; +only the target records were retained. The source URL is mutable. Its +multipart ETag is recorded as version evidence, not represented as a content +hash. The manifest also hashes the exact six-column correction rows. + +The selected changes are: + +- 642 profiles have an official name difference and a copied profile payload + associated with another canonical identity. +- 47 additional substantial name differences are classified as + **canonicalization of uncertain alias/profile**, not individually proven + wrong-person records. These use the same exact-ID official source. +- For those 689 rows, name, birth/death years, primary professions and known-for + IDs come from the source record. Unverified biography, birthplace and portrait + links are cleared. The source supplies no replacement biographies or images; + none are generated. Original image files remain in the asset archive. +- Two same-name birth-year corrections are independently supported by IMDb + profiles: [Michael Byrne, 1939](https://www.imdb.com/name/nm0126250/) and + [Matt Tarses, 1968](https://www.imdb.com/name/nm0850696/). Their other fields + are preserved; current death-year changes are not copied into the older seed. +- Fourteen name-format/diacritic/disambiguator variants are left unchanged. + `nm14177307` and `nm18523349` are missing from the consumed official dataset; + direct profile requests returned 403, so their records and credits are kept. + +For title dates, the loader prefers a valid complete `ld.datePublished`. +Legacy `Release date | Month D, YYYY ...` values are converted only when they +contain a complete, valid date. The migration normalizes 390 existing dates +without supplying new facts. The two year-only values remain unchanged: +`tt0994314` (2008) and `tt33041431` (2027). + +Two title years are corrected by exact `tconst`, using the official IMDb title +metadata observed through the search index on September 8, 2026: + +| Title | Previous `year` | Canonical `year` | Preserved regional `release_date` | +|---|---|---|---| +| [Memento](https://www.imdb.com/title/tt0209144/) | 2001 | 2000 | 2001-05-25 | +| [Schindler's List](https://www.imdb.com/title/tt0108052/) | 1994 | 1993 | 1994-02-04 | + +The Schindler's List page separately lists its United States release on +February 4, 1994. A regional release date can have a later year than the +canonical title year. These two corrections change only `titles.year`; the +regional dates and all other fields stay unchanged. The manifest records the +before/after values, source URLs, observation date and source access method. +Each `source_record_sha256` hashes the manifest's canonical JSON source record +(UTF-8, sorted keys, compact separators), not an unavailable full webpage. +No broader title-year correction or rating/gross refresh is included. + +## Reproduction and validation + +For byte-identical reproduction of the latest candidate, extract the +[previous reviewer asset revision](https://huggingface.co/datasets/ChilleD/WebHarbor/tree/4d5709e171d7c40fc742727adfa98b04b9023039) +into the checkout, then run: + +```bash +python3 sites/imdb/migrate_seed.py --report /tmp/imdb-migration-first.json +python3 sites/imdb/migrate_seed.py --report /tmp/imdb-migration-second.json +python3 -m unittest discover -s sites/imdb/tests -p test_seed_data.py +``` + +That input seed SHA256 is +`27558f13a9dd3b003435463ceaad8538e2b6f9fc5095c3bd6831666fecaab5d9`. +The original contributor revision is also a supported input and produces the +same logical database. Its single-transaction history gives different SQLite +file bytes from the previous-candidate path, so those two output hashes are +not claimed to match. + +The migration refuses to modify an unexpected source seed. Its transaction +checks the complete logical diff before committing. A completed migration +performs no writes on subsequent runs. Reports contain table/row hashes and +changed field names, not user values or old biographies. + +Verified locally with Python 3.11.3 / SQLite 3.40.1: + +- All nine targeted tests pass: canonical identity rejection/import, complete + date parsing and precedence, preserved relationships/state, byte-identical + second migration, rejection of a modified source, both supported migration + paths, and rejection of an unexpected title-year value. +- 691 person rows change: 689 selected profiles and two birth years. +- From the original seed, 390 title rows normalize `release_date`; two of those + rows also correct `year`. From the previous reviewer candidate, exactly two + `titles.year` values change and the complete remaining database is identical. +- An independent comparison against the original seed confirms all other + title/person fields and all credits, genres, news, reviews, ratings, users, + watchlists and title/genre relationships are unchanged. +- Second migration: zero changed rows and identical file SHA256. Repeating + from the same previous-candidate input produces identical output bytes. +- Original archive and the checkout's runtime database are unchanged. + +Candidate seed SHA256: +`69f849b9c61fb71349958fdbedc301162881e3a171eb65098c2e5f6623b82471`. + +These are source and migration checks. Container reset, browser behavior, +visual comparison and task execution are separate validation steps. The +removed portrait/biography links remain an explicit source-coverage limitation. diff --git a/sites/imdb/docs/starmeter-sources.json b/sites/imdb/docs/starmeter-sources.json new file mode 100644 index 00000000..a2a9bb49 --- /dev/null +++ b/sites/imdb/docs/starmeter-sources.json @@ -0,0 +1,620 @@ +{ + "archive_sha256": "e3854d4f9b8b90f9c2119db562e666b0a9cbb5811b9a4585f6c2600886d93a42", + "captured_at": "2026-09-10T15:24:24.845Z", + "features": 100, + "assets": [ + { + "path": "images/home/c2f0f295b084a3f88c910dbb.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTc5MTQ2OTYwOF5BMl5BanBnXkFtZTYwMzEzMTA1._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "c2f0f295b084a3f88c910dbb08fccab83464fd7e84b690ec38bb55b35178131b", + "bytes": 17462 + }, + { + "path": "images/home/91313498dbe7515593796211.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMDhiY2U4Y2YtODE3ZC00YjYwLWE4MWYtODA2MTlkMDc1N2FhXkEyXkFqcGc@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "91313498dbe75155937962117ab719accf489406fda408e1e8ef3841e999476d", + "bytes": 27702 + }, + { + "path": "images/home/293c06746ea678282c5a65e1.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMzUzMTNkY2QtOTBkZi00YjFlLThmMTgtM2NjNTQ1NzZiNTQ5XkEyXkFqcGc@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "293c06746ea678282c5a65e119b6e83e64a69f778e0c4fb4a0f702598fbecd3f", + "bytes": 42459 + }, + { + "path": "images/home/a8a417afe90138416ed4619a.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZmY5Y2NhMzQtYTYxNi00ZWM2LWEzYmQtNjQxMGM2MzNmMGViXkEyXkFqcGc@._V1_QL75_UY562_CR80,0,380,562_.jpg", + "sha256": "a8a417afe90138416ed4619a54614edbd6d5c52fa0c21e57a405ff1a1dd581f3", + "bytes": 42001 + }, + { + "path": "images/home/b5845df367cfedada8c6201a.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjI0NTQ4Mjk5Ml5BMl5BanBnXkFtZTcwMDc1NjkzNw@@._V1_QL75_UY562_CR6,0,380,562_.jpg", + "sha256": "b5845df367cfedada8c6201aba0356b0eeeda6535a638fdb2b3a824b6b32b94e", + "bytes": 20471 + }, + { + "path": "images/home/625257d8ba5024ad2bb9bf39.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZjY2OGYzZmMtYWQ4MC00Y2Y0LWFjYTctYzkxMmYxMTZhZGJjXkEyXkFqcGc@._V1_QL75_UY562_CR21,0,380,562_.jpg", + "sha256": "625257d8ba5024ad2bb9bf39ad7270199b483c80137ccd517c2c80c103753c9a", + "bytes": 25590 + }, + { + "path": "images/home/da4a426a5caeb6fdf5881236.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BOGY5YzFhOWMtOWRmNC00OTFlLTlmNzktMjk5OWQ4OThkNTFjXkEyXkFqcGc@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "da4a426a5caeb6fdf5881236368e0599a5910ac8b75673f0fee3a27424b63e61", + "bytes": 17455 + }, + { + "path": "images/home/df44cf1a12dcaed6864116d8.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjAwNDU3MzcyOV5BMl5BanBnXkFtZTcwMjc0MTIxMw@@._V1_QL75_UY562_CR23,0,380,562_.jpg", + "sha256": "df44cf1a12dcaed6864116d80f7e4e490f29c25784bbbd4581155fcabca25a64", + "bytes": 14874 + }, + { + "path": "images/home/0211765f48a56dff354e8425.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYmM2NDA1NmItN2MzZS00NmVlLWE5MjYtOTFkMWM2OWRmMTk1XkEyXkFqcGc@._V1_QL75_UY562_CR21,0,380,562_.jpg", + "sha256": "0211765f48a56dff354e84250643f20f4fc75400209227382cef911b07de7a9b", + "bytes": 27860 + }, + { + "path": "images/home/6cd753dff55bdd488fe41ded.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNGE0Y2E5ZjktZTA1NC00NTBkLThlNGQtODZjZDdmYzI0ODJhXkEyXkFqcGc@._V1_QL75_UX380_CR0,5,380,562_.jpg", + "sha256": "6cd753dff55bdd488fe41ded11f80b628540c2332f527717aa98d39c350a0420", + "bytes": 13791 + }, + { + "path": "images/home/57f38e3999d21756d5b60677.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZTRiM2FiMjgtZDAxOC00YzE5LThhNjktZTI0Yjg4YzY2YTU2XkEyXkFqcGc@._V1_QL75_UY562_CR72,0,380,562_.jpg", + "sha256": "57f38e3999d21756d5b60677af83dcab96bfbd063334a30b2226026578e60c98", + "bytes": 18571 + }, + { + "path": "images/home/7861fb292a6b21b9913fda14.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNTM5NDM4ODg3MV5BMl5BanBnXkFtZTcwNTA3MTk3Mw@@._V1_QL75_UY562_CR1,0,380,562_.jpg", + "sha256": "7861fb292a6b21b9913fda14c948a2b815277bcaff710f3075d252d748995d65", + "bytes": 25734 + }, + { + "path": "images/home/6703ce9bc5c7c100e87df12e.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BN2I1N2RkYWYtZWU0MC00ZDRlLWEzOGYtODkxMmJhYmYxMzM4XkEyXkFqcGc@._V1_CR179,76,815,1223_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "6703ce9bc5c7c100e87df12e05da0b2327e855d290a0eb8932385a8371710251", + "bytes": 18697 + }, + { + "path": "images/home/8bf50d2882e0bd1df7217ce9.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BM2M5YmU2NjYtZDMyMC00YzRiLWIzMWYtMThiYjNmZTg5NmUxXkEyXkFqcGc@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "8bf50d2882e0bd1df7217ce91bfcdc81a3acd98d3758ccc968a3091d120b5370", + "bytes": 32834 + }, + { + "path": "images/home/3308611b7664074b461737e0.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYWQ5MzViNjUtODdmMi00MjYxLTg0OTItM2NkOTg1YzFjYjMxXkEyXkFqcGc@._V1_CR427,126,1366,2049_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "3308611b7664074b461737e04bb408bb06fc32c7a40aebea5cbfcdb4c68704cb", + "bytes": 21060 + }, + { + "path": "images/home/e574154e4f648fee0d090951.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BODFlMTZmODItNzExZi00ZWJiLTgyMTUtOGFlY2FiZTJiZTExXkEyXkFqcGc@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "e574154e4f648fee0d0909518c2fbf553c28aa373bffaad62fc12b770fe6f797", + "bytes": 10429 + }, + { + "path": "images/home/a245458719b900164da43307.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZWVhOGE3NjYtZDlmMC00NTdhLTkzZmMtMjBlOGRmYWE0MGY5XkEyXkFqcGc@._V1_CR198,0,1375,2063_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "a245458719b900164da43307132da958755526012fe3072af370735ff048ce2a", + "bytes": 23721 + }, + { + "path": "images/home/8f2ebfb314e3e88da22ecfd1.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BY2M1MjFhZTItM2RlOS00NDU4LWE2MTEtNzE3MjZlNWRhN2Y1XkEyXkFqcGc@._V1_CR132,1,943,1414_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "8f2ebfb314e3e88da22ecfd14cce06480aeeb9ca32cd7cceb10c982c737b09f8", + "bytes": 15014 + }, + { + "path": "images/home/a0b41b8305abcd30f0785b03.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BOWQ3MzY4ZjYtOGZmZC00OTI5LTk5OTktZmE1Y2VmZGVkY2YxXkEyXkFqcGc@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "a0b41b8305abcd30f0785b03e9c7a74082db77736f501da404dffe3a60d42c58", + "bytes": 15769 + }, + { + "path": "images/home/9a3b6a3efe9628e6c110a6a2.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTUxNzMwMTU0MF5BMl5BanBnXkFtZTcwMjI2OTU5Ng@@._V1_QL75_UY562_CR3,0,380,562_.jpg", + "sha256": "9a3b6a3efe9628e6c110a6a27dc84e6ff326c3467797b6f34fe659238c63d7e3", + "bytes": 30949 + }, + { + "path": "images/home/bc6060562eeaaf3848d44b0c.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZmUzNDVkNmEtNzg5NC00YWJlLTk2NDktNzc1NzFmOGM5ZjE4XkEyXkFqcGc@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "bc6060562eeaaf3848d44b0cfcbd9016a9e2b7484ed063f06b10a0e139010ab8", + "bytes": 22877 + }, + { + "path": "images/home/d4267f58fef41f36e8736586.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BM2E2ZThiYzYtYjc4MS00YTY0LTg0ODctNWNlYzZkOWE2MWYwXkEyXkFqcGc@._V1_QL75_UY562_CR33,0,380,562_.jpg", + "sha256": "d4267f58fef41f36e87365863457278f2e94d9d7d6916e2e2ed4e6aa53e29bf2", + "bytes": 14142 + }, + { + "path": "images/home/cc2f0974d1b3cb680563ede8.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMmU1YWU1NmMtMjAyMi00MjFiLWFmZmUtOTc1ZjI5ODkxYmQyXkEyXkFqcGc@._V1_QL75_UY562_CR18,0,380,562_.jpg", + "sha256": "cc2f0974d1b3cb680563ede875d024df8acfa0a35e8bcc56b50d1132bfb9c16d", + "bytes": 28437 + }, + { + "path": "images/home/7ed4aac32cd9ea72e23adaeb.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNmQ3MmNhNTItOTVhMi00YWNhLTkwZjUtMDliMGM1YWRkNGJjXkEyXkFqcGc@._V1_QL75_UY562_CR41,0,380,562_.jpg", + "sha256": "7ed4aac32cd9ea72e23adaeb2757434dea63fb59ec2d6c932fd3039e2b88af32", + "bytes": 22674 + }, + { + "path": "images/home/d7d313d33aed019c4d8e3514.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYzU3NWRhMjgtNmNmMS00YjQ1LWIyYzgtYzdkYjRjNWEzM2E3XkEyXkFqcGc@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "d7d313d33aed019c4d8e351411ccfc06f9292b888ceac00955e4274563fb00ef", + "bytes": 19728 + }, + { + "path": "images/home/0c83e0d5d7c8b7eda2f2104a.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNDc5NTU2MzY0Nl5BMl5BanBnXkFtZTcwMzU4MjA0Ng@@._V1_QL75_UY562_CR16,0,380,562_.jpg", + "sha256": "0c83e0d5d7c8b7eda2f2104a42d7b3a5569f61707eb0d2119d3d5e3488fe513b", + "bytes": 28955 + }, + { + "path": "images/home/2e41301019547437e779ec19.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMzRiNDhiMDQtYWZkMS00ZjU5LTg5NzUtOTc4NzE2Yzc0ZWUwXkEyXkFqcGc@._V1_QL75_UY562_CR1,0,380,562_.jpg", + "sha256": "2e41301019547437e779ec196617b53364cd20aca46f9988367e74588702b6fa", + "bytes": 24529 + }, + { + "path": "images/home/9eb641a823c90906476f54aa.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjEwNzE1Mjc1MV5BMl5BanBnXkFtZTgwNTgyMjgyNzE@._V1_QL75_UY562_CR41,0,380,562_.jpg", + "sha256": "9eb641a823c90906476f54aa99fa18dba6fd13c949f03fb7efdee93feb31e541", + "bytes": 17557 + }, + { + "path": "images/home/3c924b357c2a174ed8a7db00.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNjE3NDQyOTYyMV5BMl5BanBnXkFtZTcwODcyODU2Mw@@._V1_QL75_UY562_CR13,0,380,562_.jpg", + "sha256": "3c924b357c2a174ed8a7db0096cad03538f830e91e830ef5d13b2af5cc50cda4", + "bytes": 17626 + }, + { + "path": "images/home/504f66dbb2072e67eab0c13b.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNDM1OWUyZDktZGJmYS00MjQxLWI1OTItY2M4MWViM2NmOWM0XkEyXkFqcGc@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "504f66dbb2072e67eab0c13b069479111328f63809a1054ee20429659ac98c50", + "bytes": 24024 + }, + { + "path": "images/home/f98cb6659766784c032b9935.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZjA2ODg4MTgtNzMyNS00OGM2LWJjYmYtNDI0MzIyNDdlMTdlXkEyXkFqcGc@._V1_QL75_UY562_CR38,0,380,562_.jpg", + "sha256": "f98cb6659766784c032b99350371f7f1d7982053504f512dae03d10e27c89000", + "bytes": 34215 + }, + { + "path": "images/home/f8ba501f7ab8be1e5419d09b.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BOGU1ZGViNjUtMGM1NS00MWM5LWExNWQtM2JjY2YzYzZhY2U0XkEyXkFqcGc@._V1_QL75_UY562_CR68,0,380,562_.jpg", + "sha256": "f8ba501f7ab8be1e5419d09b577de8668682ae3e05f7b6d3ff7ca41785ed5587", + "bytes": 19526 + }, + { + "path": "images/home/19a57fc7cb0f14a21fbaa4dc.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZjM5N2U3MzQtZWU5My00YzE0LThmZTgtYjE1NDJjNmIzZmIxXkEyXkFqcGc@._V1_QL75_UY562_CR21,0,380,562_.jpg", + "sha256": "19a57fc7cb0f14a21fbaa4dcdfe282737873feb80e2767ce809dd7eb7b3f6920", + "bytes": 33374 + }, + { + "path": "images/home/0f27797dc943c5c0f5ec0045.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNjk5ODc5MTU3MF5BMl5BanBnXkFtZTgwOTk2MjI5NDE@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "0f27797dc943c5c0f5ec00450a2adb1903531895ab01942579564dea3de62449", + "bytes": 31270 + }, + { + "path": "images/home/25243d44881b332b9bd21bd2.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTM2NDkxMTcxMl5BMl5BanBnXkFtZTcwNTMyNjI5MQ@@._V1_QL75_UY562_CR15,0,380,562_.jpg", + "sha256": "25243d44881b332b9bd21bd2b0d956bdfa72e2bffdc00415fc17bd349a9288de", + "bytes": 19443 + }, + { + "path": "images/home/d3b9a00cf53698fd547855ff.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjE3MzU5NDc0Nl5BMl5BanBnXkFtZTgwNDMzNjE1NzE@._V1_QL75_UY562_CR21,0,380,562_.jpg", + "sha256": "d3b9a00cf53698fd547855ffc4e3bb9554734a7c9f01e9728eeec576586603fe", + "bytes": 11497 + }, + { + "path": "images/home/01b0fe4d28fb0c8229824dcf.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZDU3OTU5MTQtNTY1My00OWViLTg2OTktNjM3NjhjZjgzNWE0XkEyXkFqcGc@._V1_QL75_UY562_CR169,0,380,562_.jpg", + "sha256": "01b0fe4d28fb0c8229824dcfedc26b0270d80eca5aea53c7fa3127b9a8ea0130", + "bytes": 17870 + }, + { + "path": "images/home/f38586a5a4222673b683715b.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTQzMjkwNTQ2OF5BMl5BanBnXkFtZTgwNTQ4MTQ4MTE@._V1_QL75_UY562_CR32,0,380,562_.jpg", + "sha256": "f38586a5a4222673b683715b864c5d7a7a903a23de667c4bfa1361aa59847130", + "bytes": 21103 + }, + { + "path": "images/home/b84c60bb6ffceee9c22acded.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTI5NDY5NjU3NF5BMl5BanBnXkFtZTcwMzQ0MTMyMw@@._V1_QL75_UY562_CR2,0,380,562_.jpg", + "sha256": "b84c60bb6ffceee9c22acdedd7daa0ac147006677a1871c2b04966b1d500beac", + "bytes": 28451 + }, + { + "path": "images/home/33d824e0138d426c067be7d1.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjA1MjE2MTQ2MV5BMl5BanBnXkFtZTcwMjE5MDY0Nw@@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "33d824e0138d426c067be7d13f35fb4816df4ad8d23d2684305982408336fda9", + "bytes": 21663 + }, + { + "path": "images/home/7e7f22580a05ddb0ecf99729.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMDVlZjIzOTktYzNmZC00MjIzLTk1YTMtMzdlOTFiNzQ3ZGY0XkEyXkFqcGc@._V1_QL75_UY562_CR56,0,380,562_.jpg", + "sha256": "7e7f22580a05ddb0ecf99729bf413e24cb7f51a890532f91f987298ddacc16a9", + "bytes": 22559 + }, + { + "path": "images/home/988750d875a47d1db999b679.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BOWIzNzJmNjMtMDY1Ni00NDkxLWExMzUtMDM4MjFjOTE5ZTMzXkEyXkFqcGc@._V1_QL75_UY562_CR115,0,380,562_.jpg", + "sha256": "988750d875a47d1db999b67946e1f3d55c5bebe87ab87eee1126205d6075bc0f", + "bytes": 28320 + }, + { + "path": "images/home/1835e95d154ca43697551dc0.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BY2UxMWE4MTAtZWE1ZC00N2E5LWFlOGItNmEyYmI3ODI4YTY5XkEyXkFqcGc@._V1_QL75_UY562_CR122,0,380,562_.jpg", + "sha256": "1835e95d154ca43697551dc030f7144c5aeb9bfefe1ab0f17a28a1e0556f3811", + "bytes": 24923 + }, + { + "path": "images/home/77bf27857597c4dd7aac0efc.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTMyMDc3Mzc2M15BMl5BanBnXkFtZTcwMjc5OTcyMg@@._V1_QL75_UY562_CR13,0,380,562_.jpg", + "sha256": "77bf27857597c4dd7aac0efcf22507fd21fe04b3d56ffcd1a4163a6739268442", + "bytes": 21581 + }, + { + "path": "images/home/e64450eaf93c5212065c0f3a.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNDEzOTdhNDUtY2EyMy00YTNmLWE5MjItZmRjMmQzYTRlMGRkXkEyXkFqcGc@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "e64450eaf93c5212065c0f3a198f66d3595fe98060f614a217696868e0479543", + "bytes": 12770 + }, + { + "path": "images/home/acb124bdeaf957e4f7c242fe.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTRlODM4M2QtOTUxMy00Y2NhLTk0ZWMtZGFjYWM2MTdmMzA0XkEyXkFqcGc@._V1_QL75_UX380_CR0,25,380,562_.jpg", + "sha256": "acb124bdeaf957e4f7c242fee64dec212d08506ceda9c785ad9a869f139e0655", + "bytes": 31348 + }, + { + "path": "images/home/7e9e9f68034cd0c0f0f41765.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTk1MjM5NDg4MF5BMl5BanBnXkFtZTcwNDg1OTQ4Nw@@._V1_QL75_UY562_CR18,0,380,562_.jpg", + "sha256": "7e9e9f68034cd0c0f0f41765e209a3c757890fea211e3bfb9a2716f6bc62357d", + "bytes": 20004 + }, + { + "path": "images/home/f4e7a75cf5fd948b537f01e3.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTM0NzYzNDgxMl5BMl5BanBnXkFtZTcwMDg2MTMyMw@@._V1_QL75_UY562_CR20,0,380,562_.jpg", + "sha256": "f4e7a75cf5fd948b537f01e380790df4b029f24ba73ef7fe716f5d73869912eb", + "bytes": 16359 + }, + { + "path": "images/home/41decb59565d2df4d7f5f76e.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTU0MTQ4OTMyMV5BMl5BanBnXkFtZTcwMTQxOTY1NA@@._V1_QL75_UY562_CR26,0,380,562_.jpg", + "sha256": "41decb59565d2df4d7f5f76e753df604ef15865648cbf8ebe3896286ebe61b97", + "bytes": 21779 + }, + { + "path": "images/home/912b3b05e92f59d625ea48c9.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BM2E5MTk3M2ItYTBlOC00MDQwLTg5OGUtMTJlMjFkZjJmNTljXkEyXkFqcGc@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "912b3b05e92f59d625ea48c9e330ea7ef2e52114abcf50d395f22865cf8eece8", + "bytes": 27557 + }, + { + "path": "images/home/a6e25ca9af64ba85edebcf0d.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTg5NTc1NzgwOV5BMl5BanBnXkFtZTcwMTcyMjIzMQ@@._V1_QL75_UY562_CR35,0,380,562_.jpg", + "sha256": "a6e25ca9af64ba85edebcf0dcb95527121e8d08aeef11bde9eab8527635a34e7", + "bytes": 21871 + }, + { + "path": "images/home/4cab36d9fca210e5d4ba2456.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BODQyN2YyMmMtZjk1OS00OGE2LTgxMjUtZTQ0NTliOGZhYWQ1XkEyXkFqcGc@._V1_CR1214,624,3333,5000_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "4cab36d9fca210e5d4ba2456e16755c1b65a9005e26d6537cf8eb19bca384b13", + "bytes": 21220 + }, + { + "path": "images/home/79b90e1d178e114774130c6d.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNzk0MDQ5OTUxMV5BMl5BanBnXkFtZTcwMDM5ODk5Mg@@._V1_QL75_UY562_CR22,0,380,562_.jpg", + "sha256": "79b90e1d178e114774130c6d8a0353567ecd75b6d5a0756ed49096ad7cbfab2c", + "bytes": 18592 + }, + { + "path": "images/home/ab2e7a78d6f38ac91b17417a.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZmIxMTFkZTctYzhkZi00MmQ4LThhYzEtMmQwMGZjNzA5MDg0XkEyXkFqcGc@._V1_QL75_UY562_CR20,0,380,562_.jpg", + "sha256": "ab2e7a78d6f38ac91b17417ab592b2064d8d90991f047591bed867133c6b1050", + "bytes": 15607 + }, + { + "path": "images/home/b3828000e6572d0893d5cce8.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNjA0MTU2NDY3MF5BMl5BanBnXkFtZTgwNDU4ODkzMzE@._V1_QL75_UX380_CR0,3,380,562_.jpg", + "sha256": "b3828000e6572d0893d5cce82ca2e385a2bace2b0cf29f0b15d3c985b0fff52b", + "bytes": 22664 + }, + { + "path": "images/home/2844e9ce389e33397a9dbb05.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjA2MjU2NjE1OV5BMl5BanBnXkFtZTgwNTMyMTAzMjI@._V1_QL75_UY562_CR248,0,380,562_.jpg", + "sha256": "2844e9ce389e33397a9dbb05eb63756b782f726d1d44607d726b1460f404000e", + "bytes": 29014 + }, + { + "path": "images/home/eb4934ecf1f2a93c231f1168.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZTIwYmNjMjctYTAyZi00Njg0LWJmZWQtZTc1MmZjZjBiMmQ4XkEyXkFqcGc@._V1_QL75_UY562_CR21,0,380,562_.jpg", + "sha256": "eb4934ecf1f2a93c231f11685bd997a1ac27a1ee0d80c89c18bb029491947c7a", + "bytes": 35742 + }, + { + "path": "images/home/c84c9590c29695860eebbfb2.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjI0MTg3MzI0M15BMl5BanBnXkFtZTcwMzQyODU2Mw@@._V1_QL75_UY562_CR19,0,380,562_.jpg", + "sha256": "c84c9590c29695860eebbfb267ad16ec31ce874858a21bb0c1a27cebf52cd423", + "bytes": 17413 + }, + { + "path": "images/home/00504abb9e3833970982b07b.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTQ3ODEyNjA4Nl5BMl5BanBnXkFtZTgwMTE4ODMyMjE@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "00504abb9e3833970982b07b0df723636bb4aff4aa669f7a4c76cd8703197f4a", + "bytes": 11563 + }, + { + "path": "images/home/421ab9e9a781db3b3ce12eec.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMDQ0ZjgzZDMtOTk2Ni00YTg2LThmMDQtNWIxZjU4ZDRkZjM4XkEyXkFqcGc@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "421ab9e9a781db3b3ce12eece46b1eff6f7019cc09139a18138060c94cb8f348", + "bytes": 15623 + }, + { + "path": "images/home/257875d16eaef61f78598933.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTgxODk4NzUwMF5BMl5BanBnXkFtZTcwMTc1MDQ4Nw@@._V1_QL75_UY562_CR31,0,380,562_.jpg", + "sha256": "257875d16eaef61f7859893390da8776b80b9c2c3280acb07aa1686144417a08", + "bytes": 28883 + }, + { + "path": "images/home/89b6c024c61c0e775828fe8b.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTcwNzA5MDg0OV5BMl5BanBnXkFtZTcwMTU2NjE0Nw@@._V1_QL75_UY562_CR16,0,380,562_.jpg", + "sha256": "89b6c024c61c0e775828fe8b0270a92d6b82cd03bd914b063f4686dcbeb70d4d", + "bytes": 16678 + }, + { + "path": "images/home/9249fb3e30103c083ad9ecd5.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjUxMjE4MTQxMF5BMl5BanBnXkFtZTcwNzc2MDM1NA@@._V1_QL75_UY562_CR16,0,380,562_.jpg", + "sha256": "9249fb3e30103c083ad9ecd5802a1b55c5fc5a93e6c53d199bb9f1ae9598f128", + "bytes": 15699 + }, + { + "path": "images/home/bde35e0d5169c1d69f873c56.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BOTUyNTBhYzYtNGEyNC00NjlhLWI4YWUtNzA5OTg1YjkyZDQxXkEyXkFqcGc@._V1_QL75_UX380_CR0,7,380,562_.jpg", + "sha256": "bde35e0d5169c1d69f873c5630e52b901b3867da21be20b37a4563770d1ca23c", + "bytes": 49271 + }, + { + "path": "images/home/0b58deac84874a192be72571.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BODRhMThiMDQtYWJiYS00MjM3LTgwNjMtNDdmMDUwNjU0YTE1XkEyXkFqcGc@._V1_QL75_UY562_CR214,0,380,562_.jpg", + "sha256": "0b58deac84874a192be7257186dc9464110c7e38be30e5400bb936523973970e", + "bytes": 21080 + }, + { + "path": "images/home/e6d97d0e4066c44208c04e43.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMDQ0YWFkYjUtMTkwNy00M2Q5LWFmYjItNWMwOTRiNzIwZmNjXkEyXkFqcGc@._V1_QL75_UY562_CR232,0,380,562_.jpg", + "sha256": "e6d97d0e4066c44208c04e43fa4bc9baaa14431ec5c8035eb541ea5b46fb12e8", + "bytes": 20257 + }, + { + "path": "images/home/6f4e9fc74391b999d8116103.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTk3MjYxNzItNzU2Zi00Y2UzLTk2YzgtNjk5NmZkZGRmOGM0XkEyXkFqcGc@._V1_QL75_UY562_CR21,0,380,562_.jpg", + "sha256": "6f4e9fc74391b999d811610322d86e7f93380b7a2c7989cfc5ef2411d8754121", + "bytes": 31549 + }, + { + "path": "images/home/fce2a28c01df13bdcfb83b65.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BODI0ZTRlM2MtNDYxNy00MWFhLWI4ODItZjllN2RlN2I5NmY4XkEyXkFqcGc@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "fce2a28c01df13bdcfb83b65fba6da2905e0d7ce2efa7292ff32a025162e9013", + "bytes": 36220 + }, + { + "path": "images/home/d187a429fd9fd05abc7eb68a.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTYwNjc5NzA2NV5BMl5BanBnXkFtZTcwNjk1MTk3Mw@@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "d187a429fd9fd05abc7eb68a569f46d0f611f7ef89c9f49e25e60cfcea4a843a", + "bytes": 40367 + }, + { + "path": "images/home/cac3d99cef85bbfd2a3b2c7e.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTU3NDUyMzE2OF5BMl5BanBnXkFtZTcwMTc0MDQ3MQ@@._V1_QL75_UY562_CR11,0,380,562_.jpg", + "sha256": "cac3d99cef85bbfd2a3b2c7e6c125a38bbb1df69a7cf1deb93a5ac6dc8035fd7", + "bytes": 22561 + }, + { + "path": "images/home/a6afbad14ebc374a3a5ce003.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTQ0MDU1NjM5OF5BMl5BanBnXkFtZTYwNTY0MzA1._V1_QL75_UX380_CR0,6,380,562_.jpg", + "sha256": "a6afbad14ebc374a3a5ce003462b752810bafa2d043c84982e24674606270556", + "bytes": 21451 + }, + { + "path": "images/home/582982e10253c4b347cbbd5d.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZjVmMGRiNmUtODVlNC00ODU4LWJlMGQtNzBkZDg4MDM4NGI2XkEyXkFqcGc@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "582982e10253c4b347cbbd5d57f5fb00dcb252044dae8ea87bda3c4cd46c1408", + "bytes": 17340 + }, + { + "path": "images/home/a1242dc32dd9b6b573f13b25.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNzlkOWQ5ZTctM2IxNS00YzIzLWFmZjAtMGNlZGJmYjQwNTI3XkEyXkFqcGc@._V1_CR393,552,1333,1999_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "a1242dc32dd9b6b573f13b253bef41d3f08e8ed24bf85b4ef3414959edd9f23d", + "bytes": 28030 + }, + { + "path": "images/home/165ffdc52bbe3d70a7a17a3d.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTY0NDY2MDQzMl5BMl5BanBnXkFtZTYwMzI1NTQ3._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "165ffdc52bbe3d70a7a17a3dab799dd1395550a8a60b3b6a402887f28b83b815", + "bytes": 27227 + }, + { + "path": "images/home/b6718503d799dd4630ef3187.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMGZjYzcxNDEtNTU1Yi00Nzc3LTlhZjQtZTUwZDQxNjQ5MDIzXkEyXkFqcGc@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "b6718503d799dd4630ef31873cfbb32a19338d14c6030a3a40bd08ff875b69cb", + "bytes": 37049 + }, + { + "path": "images/home/e817f81943c014ce039e9865.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYjgwM2FhNTMtNWNhYS00OTM2LThkNDUtZDEyYTYxM2ViMGM1XkEyXkFqcGc@._V1_QL75_UY562_CR11,0,380,562_.jpg", + "sha256": "e817f81943c014ce039e9865a1ebc829bc40288c3d6905716b6dca6e36d4a492", + "bytes": 21870 + }, + { + "path": "images/home/7acc16c3372f1167fdcee6c2.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTM2OTcyMDgxMl5BMl5BanBnXkFtZTcwNDU3NTU5OA@@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "7acc16c3372f1167fdcee6c259567f1ce20f0c83991c9791a3eea0c09c512a45", + "bytes": 17502 + }, + { + "path": "images/home/3492bb20977adca536c2c55d.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZmM3ZjE2M2QtYzljOC00ZTI4LWFhNTItOWVhNTkzM2JhOTE3XkEyXkFqcGc@._V1_QL75_UY562_CR23,0,380,562_.jpg", + "sha256": "3492bb20977adca536c2c55d0916071603dc7b0b5e951a50e82071c3be4c5269", + "bytes": 17148 + }, + { + "path": "images/home/7380f217bdf3cd51f0c71720.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMjExMjY5ODYyM15BMl5BanBnXkFtZTgwOTU0OTg0NDM@._V1_QL75_UY562_CR13,0,380,562_.jpg", + "sha256": "7380f217bdf3cd51f0c7172069be516a8b85419af604d9575b00a8d9e585ef4e", + "bytes": 23296 + }, + { + "path": "images/home/bb9cc20907c67775fd184577.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BODgwMjMwMzE2OV5BMl5BanBnXkFtZTcwOTAzMTgxMw@@._V1_QL75_UY562_CR15,0,380,562_.jpg", + "sha256": "bb9cc20907c67775fd184577e72dfe564d00a28c80e1bcbb67ce584ac17d2b59", + "bytes": 20206 + }, + { + "path": "images/home/eeae19fc6f03399b53bc7185.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNDViZTFlYWEtNDdmYi00MTBiLWFmYjktMTQwY2VhMTJmMjNkXkEyXkFqcGc@._V1_QL75_UY562_CR35,0,380,562_.jpg", + "sha256": "eeae19fc6f03399b53bc718502504904dff74b895b9ff1e43b315ded9dd6ca2f", + "bytes": 18505 + }, + { + "path": "images/home/373357e4fee27916cba8d4da.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTA2OTM4NDI3NTheQTJeQWpwZ15BbWU3MDA4NzI3Mjc@._V1_QL75_UY562_CR7,0,380,562_.jpg", + "sha256": "373357e4fee27916cba8d4da6f9025d2707d5ad2b59574ec8b669a57b04b1048", + "bytes": 28703 + }, + { + "path": "images/home/60836e7858ae81ee6c464134.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMGU3NTQ5MzgtMTE4Mi00YTEwLWIzZmItOGZhZTVkYmViNTdiXkEyXkFqcGc@._V1_QL75_UY562_CR61,0,380,562_.jpg", + "sha256": "60836e7858ae81ee6c464134f22ff0e4d2fea3697269f17ab20155850a6e5ed5", + "bytes": 12556 + }, + { + "path": "images/home/23a94d75483eb99ef60d989c.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTU5MjMxOTQyNV5BMl5BanBnXkFtZTYwNDQ3Mjg1._V1_QL75_UY562_CR28,0,380,562_.jpg", + "sha256": "23a94d75483eb99ef60d989c59517603443ada426df733f83dc0b1ee284e206e", + "bytes": 13920 + }, + { + "path": "images/home/33e5813eeb43c3c92864e70a.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTg0MDc3ODUwOV5BMl5BanBnXkFtZTcwMTk2NjY4Nw@@._V1_QL75_UX380_CR0,29,380,562_.jpg", + "sha256": "33e5813eeb43c3c92864e70ad5b49d764fce093659a3ec2a78cf3772aee6c8b1", + "bytes": 20006 + }, + { + "path": "images/home/1ebd66d96a3e9b744c73ad0a.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNjNkOTc4ZWYtM2Q2Yi00NDQzLThlMzEtMTBiYzUzN2NlZDBmXkEyXkFqcGc@._V1_QL75_UY562_CR22,0,380,562_.jpg", + "sha256": "1ebd66d96a3e9b744c73ad0a14c2c639704451c71c3e3a0bde04f74f5a3c2b78", + "bytes": 41877 + }, + { + "path": "images/home/202f682b97460fa36e33f527.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTcyNDA4Nzk3N15BMl5BanBnXkFtZTcwNDYzMjMxMw@@._V1_QL75_UX380_CR0,16,380,562_.jpg", + "sha256": "202f682b97460fa36e33f527c770712e0925b92ff74eed01d27694b09c23b8e6", + "bytes": 15406 + }, + { + "path": "images/home/d958dc4e8ce2929c80d167bc.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNGMwYmU3MTAtYjcyOC00NGMzLTgzYzItMTc4YzQwNzNhYjRmXkEyXkFqcGc@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "d958dc4e8ce2929c80d167bc6bd686c602b98d4e3c45133cbf36147912400a57", + "bytes": 31015 + }, + { + "path": "images/home/cb036da97b58813bef53f485.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BOTU2N2ZjMjAtNjk1Mi00ODM5LTk2MjItOTI3ZDQ2YWViMzljXkEyXkFqcGc@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "cb036da97b58813bef53f4859024c4613e0e2baf7bf703575d51d097178b8c97", + "bytes": 24815 + }, + { + "path": "images/home/e02be5d0de2cfe3ee6aa7024.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTgxNjUxNDk3MF5BMl5BanBnXkFtZTcwMzM0NjA0NA@@._V1_QL75_UX380_CR0,3,380,562_.jpg", + "sha256": "e02be5d0de2cfe3ee6aa7024ca11f7df88a44e9de58b4a8854cb7dcafa332346", + "bytes": 22770 + }, + { + "path": "images/home/ecd977f90e5c6b343e042f6e.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTkxMzk4MjQ4MF5BMl5BanBnXkFtZTcwMzExODQxOA@@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "ecd977f90e5c6b343e042f6e173e52cfe4c51af5fda01ee87884912416ac5599", + "bytes": 29201 + }, + { + "path": "images/home/d4dcb99145f4161487633b4a.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BOTM4Mjk5MjAwNF5BMl5BanBnXkFtZTcwNzQ3MTk3Mw@@._V1_QL75_UX380_CR0,7,380,562_.jpg", + "sha256": "d4dcb99145f4161487633b4a48dc94a3770874e7d5f864d2e71fa61701a45083", + "bytes": 30603 + }, + { + "path": "images/home/ec99366df5658256fd970f7a.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNWFmNmI2NmYtNDUyYi00MDZhLTk0ZmEtODY0MmM5NGQxMWEwXkEyXkFqcGc@._V1_QL75_UY562_CR35,0,380,562_.jpg", + "sha256": "ec99366df5658256fd970f7a013038dff6f65c4a2d11d88ebb4b87e46f05062f", + "bytes": 17767 + }, + { + "path": "images/home/2499fc00c50b3b7a6dc6901e.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTQ2MjMwNDA3Nl5BMl5BanBnXkFtZTcwMTA2NDY3NQ@@._V1_QL75_UY562_CR5,0,380,562_.jpg", + "sha256": "2499fc00c50b3b7a6dc6901e06818f87067939e8f08b6b2d92387fa6972124eb", + "bytes": 20964 + }, + { + "path": "images/home/6614c7349e929ce5e6e7c489.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMDFkMWQ5ZDItNGUzNS00YzI4LWIyOTctMDk0Mjc3MGQyZTYxXkEyXkFqcGc@._V1_QL75_UY562_CR61,0,380,562_.jpg", + "sha256": "6614c7349e929ce5e6e7c48988d7979950eff7433623426616eaef4acfb32b95", + "bytes": 28776 + }, + { + "path": "images/home/4089ce5b71034197db1f190e.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BY2M0Mjc5N2QtNGU1Mi00M2VmLWI5NTUtMzVlOTk0MGQ1YmZmXkEyXkFqcGc@._V1_QL75_UY562_CR91,0,380,562_.jpg", + "sha256": "4089ce5b71034197db1f190ee20881264b17cd7464754a51a9ed1da1f0794dd0", + "bytes": 19655 + }, + { + "path": "images/home/fee72e6ef8f62f4429d68d18.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BYTc4NWYzZmQtNjBiZi00NTBmLTk2NjAtZjIwZTYyYmEwOWJkXkEyXkFqcGc@._V1_QL75_UY562_CR35,0,380,562_.jpg", + "sha256": "fee72e6ef8f62f4429d68d18123a6936982c2e609704070c9146e8c20c7905b4", + "bytes": 13936 + }, + { + "path": "images/home/5d290fad1d56625b400d5605.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BZDI0MWFhMDgtOTY0MS00MWRkLTljYmItYTBlNDg3NzI0N2U1XkEyXkFqcGc@._V1_QL75_UY562_CR35,0,380,562_.jpg", + "sha256": "5d290fad1d56625b400d56050968cafb590912b2d4eb5004aa97175b4cecffd6", + "bytes": 23922 + }, + { + "path": "images/home/321e275c87fb37a1f3c02380.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BMTI4NzAyMTE2N15BMl5BanBnXkFtZTcwNzUyODA3MQ@@._V1_QL75_UY562_CR13,0,380,562_.jpg", + "sha256": "321e275c87fb37a1f3c02380eca31b3ff0e0fed670d0977c64650bec85ed65f7", + "bytes": 19408 + }, + { + "path": "images/home/50be505ecd9efa599c1da49d.jpg", + "source_url": "https://m.media-amazon.com/images/M/MV5BNmQyYzI0YzgtNTY2Yy00ZWQ3LTkwMmItZGJjMGJhMTk2NTI0XkEyXkFqcGc@._V1_QL75_UX380_CR0,4,380,562_.jpg", + "sha256": "50be505ecd9efa599c1da49d1eb9de4f594cf7479504aa83f65ae8499e751720", + "bytes": 20729 + } + ], + "protected_tables_unchanged": [ + "users", + "titles", + "persons", + "genres", + "news_items", + "title_genre", + "credits", + "reviews", + "user_ratings", + "watchlist_items" + ], + "seed_sha256": "f17a59886b915532195c2c178c4af0b07df2f13a2a0f2d5acc9d51bb1de7cc25" +} diff --git a/sites/imdb/migrate_seed.py b/sites/imdb/migrate_seed.py new file mode 100644 index 00000000..7a058203 --- /dev/null +++ b/sites/imdb/migrate_seed.py @@ -0,0 +1,189 @@ +"""Apply the sourced IMDb corrections once, before packaging the HF seed. + +This is an offline asset migration, never a runtime/reset hook. It preserves +identifiers, relationships and user state. Only explicitly hashed source seeds +can be changed; a fully corrected seed is an exact byte-preserving no-op. +""" +import argparse +from collections import Counter +from contextlib import closing +import hashlib +import json +from pathlib import Path +import re +import sqlite3 + +from seed_data import _parse_release_date + +BASE_DIR = Path(__file__).resolve().parent +MANIFEST = BASE_DIR / 'seed_corrections.json' +PERSON_FIELDS = ('name', 'birth_year', 'death_year', 'birth_place', 'bio', + 'primary_profession', 'photo_path', 'known_for_json') + + +def _hash(value): + encoded = json.dumps(value, ensure_ascii=False, separators=(',', ':')).encode() + return hashlib.sha256(encoded).hexdigest() + + +def _snapshot(connection): + """Keep row values in memory; exported reports contain only their hashes.""" + schema = connection.execute( + "SELECT type,name,tbl_name,sql FROM sqlite_master ORDER BY type,name").fetchall() + tables = {} + for (name,) in connection.execute( + "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"): + quoted = '"' + name.replace('"', '""') + '"' + cursor = connection.execute(f'SELECT * FROM {quoted} ORDER BY rowid') + tables[name] = {'columns': [column[0] for column in cursor.description], + 'rows': cursor.fetchall()} + return {'schema': schema, 'tables': tables} + + +def _person_values(record): + def year(value): + return None if value == '\\N' else int(value) + professions = record['primaryProfession'] + known_for = record['knownForTitles'] + return dict(zip(PERSON_FIELDS, ( + record['primaryName'], year(record['birthYear']), year(record['deathYear']), + '', '', '' if professions == '\\N' else ', '.join( + part.replace('_', ' ').title() for part in professions.split(',')), + '', json.dumps([] if known_for == '\\N' else known_for.split(','))))) + + +def _prepare_updates(connection, manifest): + updates = [] + seen = set() + groups = ((manifest['people'], _person_values), + (manifest['birth_year_corrections'], + lambda row: {'birth_year': int(row['birthYear'])})) + for records, values_for in groups: + for record in records: + nm_id = record['nconst'] + if not re.fullmatch(r'nm\d+', nm_id) or nm_id in seen: + raise ValueError('Invalid or duplicate correction identity') + seen.add(nm_id) + row = connection.execute('SELECT * FROM persons WHERE nm_id=?', (nm_id,)) + columns = [column[0] for column in row.description] + original = row.fetchone() + if original is None: + raise ValueError(f'Correction identity missing from source seed: {nm_id}') + current = dict(zip(columns, original)) + changes = {field: value for field, value in values_for(record).items() + if current[field] != value} + if changes: + updates.append(('persons', current['id'], nm_id, changes)) + title_years = {} + for record in manifest.get('title_year_corrections', []): + tt_id = record['tconst'] + if (not re.fullmatch(r'tt\d+', tt_id) or tt_id in title_years + or type(record['before']) is not int or type(record['after']) is not int): + raise ValueError('Invalid or duplicate title-year correction') + title_years[tt_id] = record + for row_id, tt_id, original, year in connection.execute('SELECT id,tt_id,release_date,year FROM titles'): + changes = {} + parsed = _parse_release_date(original) + if parsed and parsed != original: + changes['release_date'] = parsed + correction = title_years.pop(tt_id, None) + if correction: + if year not in (correction['before'], correction['after']): + raise ValueError(f'Unexpected title year for correction: {tt_id}') + if year != correction['after']: + changes['year'] = correction['after'] + if changes: + updates.append(('titles', row_id, tt_id, changes)) + if title_years: + raise ValueError('Correction title missing from source seed') + return updates + + +def _verify_logical_diff(before, after, updates): + if before['schema'] != after['schema'] or before['tables'].keys() != after['tables'].keys(): + raise ValueError('Migration changed database schema') + expected = {(table, row_id): (key, values) for table, row_id, key, values in updates} + changes = [] + for table, original in before['tables'].items(): + current = after['tables'][table] + if original['columns'] != current['columns'] or len(original['rows']) != len(current['rows']): + raise ValueError(f'Migration changed row/column structure: {table}') + columns = original['columns'] + for old, new in zip(original['rows'], current['rows']): + if old == new: + continue + row_id = old[columns.index('id')] + key, permitted = expected.pop((table, row_id), (None, {})) + actual = {field: value for field, previous, value in zip(columns, old, new) + if previous != value} + if actual != permitted: + raise ValueError(f'Unexpected field changes in {table} row {row_id}') + changes.append({'table': table, 'row_id': row_id, 'key': key, + 'fields': sorted(actual), 'before_row_sha256': _hash(old), + 'after_row_sha256': _hash(new)}) + if expected: + raise ValueError('Expected updates missing from logical diff') + return changes + + +def migrate_seed(database, manifest_path=MANIFEST): + database = Path(database).resolve() + manifest = json.loads(Path(manifest_path).read_text()) + if manifest.get('schema_version') != 1: + raise ValueError('Unsupported correction manifest schema') + before_bytes = hashlib.sha256(database.read_bytes()).hexdigest() + with closing(sqlite3.connect(database.as_uri() + '?mode=rw', uri=True)) as connection: + before = _snapshot(connection) + updates = _prepare_updates(connection, manifest) + accepted_sources = {manifest['source_seed']['sha256']} + accepted_sources.update(source['sha256'] for source in manifest.get('additional_source_seeds', [])) + if updates and before_bytes not in accepted_sources: + raise ValueError('Refusing to change an unexpected source seed') + if updates: + try: + connection.execute('BEGIN IMMEDIATE') + for table, row_id, _, values in updates: + columns = ', '.join(f'{field}=?' for field in values) + connection.execute(f'UPDATE {table} SET {columns} WHERE id=?', + (*values.values(), row_id)) + after = _snapshot(connection) + changes = _verify_logical_diff(before, after, updates) + connection.commit() + except Exception: + connection.rollback() + raise + else: + after, changes = before, [] + return { + 'before_sha256': before_bytes, + 'after_sha256': hashlib.sha256(database.read_bytes()).hexdigest(), + 'manifest_sha256': hashlib.sha256(Path(manifest_path).read_bytes()).hexdigest(), + 'changed_rows': dict(Counter(change['table'] for change in changes)), + 'changed_fields': dict(Counter(f"{change['table']}.{field}" + for change in changes for field in change['fields'])), + 'logical_diff': changes, + 'tables': {table: {'row_count': len(snapshot['rows']), + 'before_sha256': _hash(snapshot), + 'after_sha256': _hash(after['tables'][table])} + for table, snapshot in before['tables'].items()}, + 'protected_tables_unchanged': [table for table in before['tables'] + if table not in ('persons', 'titles')], + } + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('database', nargs='?', type=Path, + default=BASE_DIR / 'instance_seed' / 'imdb.db') + parser.add_argument('--manifest', type=Path, default=MANIFEST) + parser.add_argument('--report', type=Path) + args = parser.parse_args() + report = migrate_seed(args.database, args.manifest) + if args.report: + args.report.write_text(json.dumps(report, indent=2) + '\n') + print(json.dumps({key: report[key] for key in + ('before_sha256', 'after_sha256', 'changed_rows', 'changed_fields')})) + + +if __name__ == '__main__': + main() diff --git a/sites/imdb/requirements.txt b/sites/imdb/requirements.txt new file mode 100644 index 00000000..e3e9a71d --- /dev/null +++ b/sites/imdb/requirements.txt @@ -0,0 +1 @@ +Flask diff --git a/sites/imdb/seed_corrections.json b/sites/imdb/seed_corrections.json new file mode 100644 index 00000000..2296fa97 --- /dev/null +++ b/sites/imdb/seed_corrections.json @@ -0,0 +1,778 @@ +{ + "schema_version": 1, + "source_seed": { + "repository": "ChilleD/WebHarbor", + "revision": "51c523d1c02a57caaaea44c76f3f96646d15bcea", + "archive": "imdb.tar.gz", + "archive_sha256": "333800bc9919c6a7f258e8e49cca611929886f6dfe222d02773006b5a5368f75", + "member": "imdb/instance_seed/imdb.db", + "sha256": "f9f1431142d528c3126663a452dc12df0a8cd952f49b0b0be5fb807fd8bb5abf" + }, + "additional_source_seeds": [ + { + "repository": "ChilleD/WebHarbor", + "revision": "4d5709e171d7c40fc742727adfa98b04b9023039", + "archive": "imdb.tar.gz", + "archive_sha256": "80fbfed63f66312e2a0c853972d17652c4ada8b2790cda8c4d7286ee667cb9b2", + "member": "imdb/instance_seed/imdb.db", + "sha256": "27558f13a9dd3b003435463ceaad8538e2b6f9fc5095c3bd6831666fecaab5d9", + "description": "Previously reviewed profile/date correction candidate; only two title.year changes remain." + } + ], + "title_year_corrections": [ + { + "tconst": "tt0209144", + "title": "Memento", + "before": 2001, + "after": 2000, + "preserved_release_date": "2001-05-25", + "source": { + "url": "https://www.imdb.com/title/tt0209144/", + "observed_on": "2026-09-08", + "access": "official IMDb title search index; not a direct page screenshot", + "fact": "Memento (2000)" + }, + "source_record_sha256": "c3e03479dc79666e3072f9277fcdb1a99c38b98a61657baef7ca02f715f5e984" + }, + { + "tconst": "tt0108052", + "title": "Schindler's List", + "before": 1994, + "after": 1993, + "preserved_release_date": "1994-02-04", + "source": { + "url": "https://www.imdb.com/title/tt0108052/", + "observed_on": "2026-09-08", + "access": "official IMDb title search index; not a direct page screenshot", + "fact": "Schindler's List (1993); United States release February 4, 1994" + }, + "source_record_sha256": "13e7607b15a0900d449fa60d03d4d3ee70838f3bc1091ad835689fa6c048c17c" + } + ], + "official_source": { + "description_url": "https://data.imdb.com/non-commercial-datasets/", + "url": "https://datasets.imdbws.com/name.basics.tsv.gz", + "retrieved_at": "2026-09-07T17:07:30.738877+00:00", + "last_modified": "Mon, 07 Sep 2026 00:32:01 GMT", + "run_date": "2026-09-06", + "etag": "\"f4f943948657f323e733cd38bac4c4ea-37\"", + "compressed_bytes": 309376069, + "compressed_sha256": "8381884fe8796b09d1f6f6f2871ab6ccdfc7dfbc1a10fada14d45a3f98307aea", + "correction_rows_tsv_sha256": "f056e53001b0d2883692dfe98cd71b1a93daae42c30e9e778a1366e5d97ac595", + "validation": "Full gzip consumed to EOF; CRC and Content-Length validated. URL is mutable; ETag is multipart and is not a content hash." + }, + "scope": { + "profile_collision": 642, + "canonicalization_of_uncertain_alias_or_profile": 47, + "same_identity_birth_year_only": 2, + "exact_title_canonical_year_only": 2, + "policy": "Match exact nconst only. Correct selected profile facts; remove unverifiable biography, birthplace, and portrait links. Preserve format/diacritic variants and source-missing identities. Convert only existing complete release dates to ISO; retain unknown/partial dates. Correct the two explicitly sourced canonical title years while preserving regional release dates." + }, + "birth_year_corrections": [ + { + "nconst": "nm0126250", + "birthYear": "1939", + "source_url": "https://www.imdb.com/name/nm0126250/", + "source_fact": "Born November 7, 1939", + "observed_at": "2026-09-07T17:11:19Z" + }, + { + "nconst": "nm0850696", + "birthYear": "1968", + "source_url": "https://www.imdb.com/name/nm0850696/", + "source_fact": "Born September 21, 1968", + "observed_at": "2026-09-07T17:11:19Z" + } + ], + "people": [ + {"nconst":"nm0000006","primaryName":"Ingrid Bergman","birthYear":"1915","deathYear":"1982","primaryProfession":"actress,producer,soundtrack","knownForTitles":"tt0034583,tt0038109,tt0036855,tt0038787","kind":"profile_collision"}, + {"nconst":"nm0000007","primaryName":"Humphrey Bogart","birthYear":"1899","deathYear":"1957","primaryProfession":"actor,producer,miscellaneous","knownForTitles":"tt0034583,tt0043265,tt0037382,tt0042593","kind":"profile_collision"}, + {"nconst":"nm0000020","primaryName":"Henry Fonda","birthYear":"1905","deathYear":"1982","primaryProfession":"actor,producer,miscellaneous","knownForTitles":"tt0050083,tt0082846,tt0032551,tt0036244","kind":"profile_collision"}, + {"nconst":"nm0000041","primaryName":"Akira Kurosawa","birthYear":"1910","deathYear":"1998","primaryProfession":"writer,director,assistant_director","knownForTitles":"tt0051808,tt0080979,tt0089881,tt0057565","kind":"profile_collision"}, + {"nconst":"nm0000044","primaryName":"Burt Lancaster","birthYear":"1913","deathYear":"1994","primaryProfession":"actor,producer,miscellaneous","knownForTitles":"tt0045793,tt0057091,tt0085859,tt0053793","kind":"profile_collision"}, + {"nconst":"nm0000059","primaryName":"Laurence Olivier","birthYear":"1907","deathYear":"1989","primaryProfession":"actor,producer,director","knownForTitles":"tt0069281,tt0032976,tt0077269,tt0074860","kind":"profile_collision"}, + {"nconst":"nm0000063","primaryName":"Anthony Quinn","birthYear":"1915","deathYear":"2001","primaryProfession":"actor,producer,director","knownForTitles":"tt0057831,tt0056172,tt0049456,tt0036244","kind":"profile_collision"}, + {"nconst":"nm0000071","primaryName":"James Stewart","birthYear":"1908","deathYear":"1997","primaryProfession":"actor,director,producer","knownForTitles":"tt0052357,tt0052561,tt0038650,tt0031679","kind":"profile_collision"}, + {"nconst":"nm0000093","primaryName":"Brad Pitt","birthYear":"1963","deathYear":"\\N","primaryProfession":"producer,actor,executive","knownForTitles":"tt0137523,tt0356910,tt0114746,tt1210166","kind":"profile_collision"}, + {"nconst":"nm0000111","primaryName":"Matthew Broderick","birthYear":"1962","deathYear":"\\N","primaryProfession":"actor,director,producer","knownForTitles":"tt0395251,tt0091042,tt0086567,tt0126886","kind":"profile_collision"}, + {"nconst":"nm0000118","primaryName":"John Carpenter","birthYear":"1948","deathYear":"\\N","primaryProfession":"music_department,writer,composer","knownForTitles":"tt0080749,tt0077651,tt0069945,tt0082340","kind":"profile_collision"}, + {"nconst":"nm0000141","primaryName":"David Duchovny","birthYear":"1960","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt0106179,tt0120902,tt0904208,tt0443701","kind":"profile_collision"}, + {"nconst":"nm0000147","primaryName":"Colin Firth","birthYear":"1960","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt1315981,tt1504320,tt0243155,tt2802144","kind":"profile_collision"}, + {"nconst":"nm0000150","primaryName":"Michael J. Fox","birthYear":"1961","deathYear":"\\N","primaryProfession":"actor,producer,miscellaneous","knownForTitles":"tt0088763,tt0096874,tt0116365,tt0115369","kind":"profile_collision"}, + {"nconst":"nm0000155","primaryName":"Whoopi Goldberg","birthYear":"1955","deathYear":"\\N","primaryProfession":"actress,producer,writer","knownForTitles":"tt0099653,tt0105417,tt0088939,tt0108147","kind":"profile_collision"}, + {"nconst":"nm0000157","primaryName":"Linda Hamilton","birthYear":"1956","deathYear":"\\N","primaryProfession":"actress,soundtrack,archive_footage","knownForTitles":"tt0103064,tt6450804,tt0088247,tt0118928","kind":"profile_collision"}, + {"nconst":"nm0000164","primaryName":"Anthony Hopkins","birthYear":"1937","deathYear":"\\N","primaryProfession":"actor,composer,producer","knownForTitles":"tt0102926,tt0113987,tt0412080,tt0975645","kind":"profile_collision"}, + {"nconst":"nm0000172","primaryName":"Harvey Keitel","birthYear":"1939","deathYear":"\\N","primaryProfession":"actor,producer,soundtrack","knownForTitles":"tt0105236,tt0103759,tt2278388,tt0110912","kind":"profile_collision"}, + {"nconst":"nm0000173","primaryName":"Nicole Kidman","birthYear":"1967","deathYear":"\\N","primaryProfession":"actress,producer,music_department","knownForTitles":"tt0203009,tt0114681,tt0274558,tt0935075","kind":"profile_collision"}, + {"nconst":"nm0000175","primaryName":"Stephen King","birthYear":"1947","deathYear":"\\N","primaryProfession":"writer,producer,actor","knownForTitles":"tt0091499,tt0083767,tt0105428,tt0111161","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0000184","primaryName":"George Lucas","birthYear":"1944","deathYear":"\\N","primaryProfession":"writer,producer,director","knownForTitles":"tt0076759,tt0066434,tt0069704,tt0082971","kind":"profile_collision"}, + {"nconst":"nm0000234","primaryName":"Charlize Theron","birthYear":"1975","deathYear":"\\N","primaryProfession":"producer,actress,costume_designer","knownForTitles":"tt0340855,tt1392190,tt5610554,tt1735898","kind":"profile_collision"}, + {"nconst":"nm0000242","primaryName":"Mark Wahlberg","birthYear":"1971","deathYear":"\\N","primaryProfession":"producer,actor,composer","knownForTitles":"tt0964517,tt0118749,tt0407887,tt1637725","kind":"profile_collision"}, + {"nconst":"nm0000255","primaryName":"Ben Affleck","birthYear":"1972","deathYear":"\\N","primaryProfession":"producer,actor,writer","knownForTitles":"tt1024648,tt2267998,tt0840361,tt0119217","kind":"profile_collision"}, + {"nconst":"nm0000261","primaryName":"Karen Allen","birthYear":"1951","deathYear":"\\N","primaryProfession":"actress,director,soundtrack","knownForTitles":"tt0082971,tt0367882,tt0088172,tt0077975","kind":"profile_collision"}, + {"nconst":"nm0000293","primaryName":"Sean Bean","birthYear":"1959","deathYear":"\\N","primaryProfession":"actor,producer,animation_department","knownForTitles":"tt0120737,tt1181791,tt0332452,tt1617661","kind":"profile_collision"}, + {"nconst":"nm0000297","primaryName":"Tom Berenger","birthYear":"1949","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt0091763,tt1375666,tt0085244,tt0097815","kind":"profile_collision"}, + {"nconst":"nm0000305","primaryName":"Mel Blanc","birthYear":"1908","deathYear":"1989","primaryProfession":"actor,music_department,miscellaneous","knownForTitles":"tt0096438,tt0094939,tt0078915,tt0077278","kind":"profile_collision"}, + {"nconst":"nm0000307","primaryName":"Helena Bonham Carter","birthYear":"1966","deathYear":"\\N","primaryProfession":"actress,producer,director","knownForTitles":"tt0408236,tt1707386,tt0137523,tt1504320","kind":"profile_collision"}, + {"nconst":"nm0000314","primaryName":"Charles Bronson","birthYear":"1921","deathYear":"2003","primaryProfession":"actor,archive_footage","knownForTitles":"tt0071402,tt0064116,tt0057115,tt0054047","kind":"profile_collision"}, + {"nconst":"nm0000342","primaryName":"James Cromwell","birthYear":"1940","deathYear":"\\N","primaryProfession":"actor,producer,soundtrack","knownForTitles":"tt0119488,tt0120689,tt0343818,tt0398165","kind":"profile_collision"}, + {"nconst":"nm0000349","primaryName":"Joan Cusack","birthYear":"1962","deathYear":"\\N","primaryProfession":"actress,writer,soundtrack","knownForTitles":"tt0119360,tt0096463,tt0119229,tt1659337","kind":"profile_collision"}, + {"nconst":"nm0000353","primaryName":"Willem Dafoe","birthYear":"1955","deathYear":"\\N","primaryProfession":"actor,producer,miscellaneous","knownForTitles":"tt0145487,tt6938828,tt5649144,tt7984734","kind":"profile_collision"}, + {"nconst":"nm0000354","primaryName":"Matt Damon","birthYear":"1970","deathYear":"\\N","primaryProfession":"producer,actor,writer","knownForTitles":"tt0119217,tt3659388,tt0407887,tt0134119","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0000407","primaryName":"Vivica A. Fox","birthYear":"1964","deathYear":"\\N","primaryProfession":"actress,producer,director","knownForTitles":"tt0116629,tt0266697,tt0373474,tt0118688","kind":"profile_collision"}, + {"nconst":"nm0000416","primaryName":"Terry Gilliam","birthYear":"1940","deathYear":"\\N","primaryProfession":"director,writer,actor","knownForTitles":"tt0088846,tt0071853,tt0081633,tt0085959","kind":"profile_collision"}, + {"nconst":"nm0000418","primaryName":"Danny Glover","birthYear":"1946","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt0093409,tt0104714,tt0122151,tt0097733","kind":"profile_collision"}, + {"nconst":"nm0000454","primaryName":"Dennis Hopper","birthYear":"1936","deathYear":"2010","primaryProfession":"actor,director,writer","knownForTitles":"tt0064276,tt0090756,tt0111257,tt0114898","kind":"profile_collision"}, + {"nconst":"nm0000474","primaryName":"Michael Keaton","birthYear":"1951","deathYear":"\\N","primaryProfession":"actor,production_department,producer","knownForTitles":"tt2562232,tt1895587,tt4276820,tt0094721","kind":"profile_collision"}, + {"nconst":"nm0000501","primaryName":"Ray Liotta","birthYear":"1954","deathYear":"2022","primaryProfession":"actor,producer,soundtrack","knownForTitles":"tt0099685,tt0097351,tt0272207,tt0091983","kind":"profile_collision"}, + {"nconst":"nm0000505","primaryName":"Nia Long","birthYear":"1970","deathYear":"\\N","primaryProfession":"actress,producer,director","knownForTitles":"tt2083355,tt0168501,tt0208003,tt0181984","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0000506","primaryName":"Julia Louis-Dreyfus","birthYear":"1961","deathYear":"\\N","primaryProfession":"actress,producer,writer","knownForTitles":"tt0098904,tt2390361,tt1759761,tt0462128","kind":"profile_collision"}, + {"nconst":"nm0000507","primaryName":"Rob Lowe","birthYear":"1964","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt0090060,tt1291580,tt0086066,tt0145660","kind":"profile_collision"}, + {"nconst":"nm0000546","primaryName":"Matthew Modine","birthYear":"1959","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt0093058,tt0112760,tt0086969,tt1345836","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0000554","primaryName":"Sam Neill","birthYear":"1947","deathYear":"2026","primaryProfession":"actor,producer,director","knownForTitles":"tt4698684,tt0104850,tt0107290,tt0163025","kind":"profile_collision"}, + {"nconst":"nm0000568","primaryName":"Frank Oz","birthYear":"1944","deathYear":"\\N","primaryProfession":"actor,miscellaneous,director","knownForTitles":"tt0074028,tt0063951,tt0086190,tt0080684","kind":"profile_collision"}, + {"nconst":"nm0000579","primaryName":"Ron Perlman","birthYear":"1950","deathYear":"\\N","primaryProfession":"actor,producer,executive","knownForTitles":"tt3973768,tt0411477,tt0167190,tt0092319","kind":"profile_collision"}, + {"nconst":"nm0000587","primaryName":"Donald Pleasence","birthYear":"1919","deathYear":"1995","primaryProfession":"actor,writer,director","knownForTitles":"tt0082495,tt0077651,tt0057115,tt0062512","kind":"profile_collision"}, + {"nconst":"nm0000632","primaryName":"Jerry Seinfeld","birthYear":"1954","deathYear":"\\N","primaryProfession":"producer,writer,actor","knownForTitles":"tt0098904,tt0389790,tt14914430,tt2314952","kind":"profile_collision"}, + {"nconst":"nm0000642","primaryName":"Marina Sirtis","birthYear":"1955","deathYear":"\\N","primaryProfession":"actress,producer,soundtrack","knownForTitles":"tt0092455,tt0111280,tt0375679,tt0117731","kind":"profile_collision"}, + {"nconst":"nm0000653","primaryName":"Brent Spiner","birthYear":"1949","deathYear":"\\N","primaryProfession":"actor,director,writer","knownForTitles":"tt0117731,tt1628841,tt0253754,tt0111280","kind":"profile_collision"}, + {"nconst":"nm0000685","primaryName":"Jon Voight","birthYear":"1938","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt0064665,tt0118615,tt0068473,tt0418279","kind":"profile_collision"}, + {"nconst":"nm0000697","primaryName":"Billy Wilder","birthYear":"1906","deathYear":"2002","primaryProfession":"writer,director,producer","knownForTitles":"tt0053604,tt0053291,tt0047437,tt0043014","kind":"profile_collision"}, + {"nconst":"nm0000703","primaryName":"BD Wong","birthYear":"1960","deathYear":"\\N","primaryProfession":"actor,director,soundtrack","knownForTitles":"tt0107290,tt4158110,tt0203259,tt0120762","kind":"profile_collision"}, + {"nconst":"nm0000709","primaryName":"Robert Zemeckis","birthYear":"1951","deathYear":"\\N","primaryProfession":"producer,writer,director","knownForTitles":"tt0088763,tt0109830,tt0118884,tt1907668","kind":"profile_collision"}, + {"nconst":"nm0000719","primaryName":"F. Murray Abraham","birthYear":"1939","deathYear":"\\N","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0086879,tt2278388,tt0113819,tt0120844","kind":"profile_collision"}, + {"nconst":"nm0000741","primaryName":"Tim Allen","birthYear":"1953","deathYear":"\\N","primaryProfession":"actor,producer,miscellaneous","knownForTitles":"tt0114709,tt0101120,tt0177789,tt0452681","kind":"profile_collision"}, + {"nconst":"nm0000751","primaryName":"Suzy Amis","birthYear":"1962","deathYear":"\\N","primaryProfession":"actress,producer,soundtrack","knownForTitles":"tt0114814,tt0109303,tt0120338,tt0106350","kind":"profile_collision"}, + {"nconst":"nm0000812","primaryName":"Roger Avary","birthYear":"1965","deathYear":"\\N","primaryProfession":"producer,writer,director","knownForTitles":"tt0110912,tt0105236,tt0110265,tt0442933","kind":"profile_collision"}, + {"nconst":"nm0000867","primaryName":"Jason Bateman","birthYear":"1969","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt0367279,tt2948356,tt2170299,tt2704998","kind":"profile_collision"}, + {"nconst":"nm0000895","primaryName":"Barbara Bel Geddes","birthYear":"1922","deathYear":"2005","primaryProfession":"actress,soundtrack,archive_footage","knownForTitles":"tt0052357,tt0077000,tt0042832,tt0040221","kind":"profile_collision"}, + {"nconst":"nm0000905","primaryName":"Roberto Benigni","birthYear":"1952","deathYear":"\\N","primaryProfession":"actor,writer,director","knownForTitles":"tt0118799,tt0419198,tt0090967,tt0102164","kind":"profile_collision"}, + {"nconst":"nm0000937","primaryName":"Richard Beymer","birthYear":"1939","deathYear":"\\N","primaryProfession":"actor,cinematographer,editor","knownForTitles":"tt0055614,tt0098936,tt0056197,tt0226836","kind":"profile_collision"}, + {"nconst":"nm0000981","primaryName":"James Brolin","birthYear":"1940","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt0181865,tt0077294,tt0078767,tt0070909","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0000985","primaryName":"James L. Brooks","birthYear":"1940","deathYear":"\\N","primaryProfession":"writer,producer,miscellaneous","knownForTitles":"tt0092699,tt0119822,tt0086425,tt0371246","kind":"profile_collision"}, + {"nconst":"nm0001003","primaryName":"Simon Callow","birthYear":"1949","deathYear":"\\N","primaryProfession":"actor,writer,director","knownForTitles":"tt0109831,tt0091867,tt0138097,tt0293508","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0001016","primaryName":"David Carradine","birthYear":"1936","deathYear":"2009","primaryProfession":"actor,producer,director","knownForTitles":"tt0378194,tt0074235,tt0072856,tt0490086","kind":"profile_collision"}, + {"nconst":"nm0001075","primaryName":"Peter Coyote","birthYear":"1941","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt0104779,tt0281358,tt0195685,tt0129290","kind":"profile_collision"}, + {"nconst":"nm0001079","primaryName":"Scatman Crothers","birthYear":"1910","deathYear":"1986","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0081505,tt0073486,tt0086491,tt0065421","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0001101","primaryName":"Ted Danson","birthYear":"1947","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt0083399,tt4955642,tt0082089,tt0098966","kind":"profile_collision"}, + {"nconst":"nm0001104","primaryName":"Frank Darabont","birthYear":"1959","deathYear":"\\N","primaryProfession":"writer,producer,director","knownForTitles":"tt0111161,tt0120689,tt0884328,tt1520211","kind":"profile_collision"}, + {"nconst":"nm0001120","primaryName":"Vittorio De Sica","birthYear":"1901","deathYear":"1974","primaryProfession":"actor,director,writer","knownForTitles":"tt0040522,tt0045274,tt0047313,tt0043809","kind":"profile_collision"}, + {"nconst":"nm0001159","primaryName":"Faye Dunaway","birthYear":"1941","deathYear":"\\N","primaryProfession":"actress,producer,director","knownForTitles":"tt0061418,tt0074958,tt0071315,tt0088206","kind":"profile_collision"}, + {"nconst":"nm0001160","primaryName":"Adrian Dunbar","birthYear":"1958","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt0104036,tt0102014,tt0097937,tt2303687","kind":"profile_collision"}, + {"nconst":"nm0001187","primaryName":"Noah Emmerich","birthYear":"1965","deathYear":"\\N","primaryProfession":"actor,director,archive_footage","knownForTitles":"tt1650062,tt0120382,tt0349825,tt0115639","kind":"profile_collision"}, + {"nconst":"nm0001232","primaryName":"Miloš Forman","birthYear":"1932","deathYear":"2018","primaryProfession":"director,writer,actor","knownForTitles":"tt0073486,tt0086879,tt0117318,tt0171433","kind":"profile_collision"}, + {"nconst":"nm0001233","primaryName":"Robert Forster","birthYear":"1941","deathYear":"2019","primaryProfession":"actor,producer,director","knownForTitles":"tt0119396,tt1033575,tt0166924,tt2302755","kind":"profile_collision"}, + {"nconst":"nm0001290","primaryName":"Richard E. Grant","birthYear":"1957","deathYear":"\\N","primaryProfession":"actor,director,writer","knownForTitles":"tt0280707,tt4595882,tt0102070,tt17351924","kind":"profile_collision"}, + {"nconst":"nm0001291","primaryName":"Rupert Graves","birthYear":"1963","deathYear":"\\N","primaryProfession":"actor,writer,director","knownForTitles":"tt0434409,tt0091867,tt0093512,tt0795368","kind":"profile_collision"}, + {"nconst":"nm0001327","primaryName":"David Hasselhoff","birthYear":"1952","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt0096542,tt0083437,tt1714203,tt3896198","kind":"profile_collision"}, + {"nconst":"nm0001354","primaryName":"Ciarán Hinds","birthYear":"1953","deathYear":"\\N","primaryProfession":"actor,producer,soundtrack","knownForTitles":"tt12789558,tt1596365,tt1340800,tt2294629","kind":"profile_collision"}, + {"nconst":"nm0001392","primaryName":"Peter Jackson","birthYear":"1961","deathYear":"\\N","primaryProfession":"producer,director,writer","knownForTitles":"tt0120737,tt0092610,tt0360717,tt0110005","kind":"profile_collision"}, + {"nconst":"nm0001426","primaryName":"Ben Kingsley","birthYear":"1943","deathYear":"\\N","primaryProfession":"actor,producer,miscellaneous","knownForTitles":"tt0203119,tt0083987,tt0108052,tt1300854","kind":"profile_collision"}, + {"nconst":"nm0001430","primaryName":"Jack Klugman","birthYear":"1922","deathYear":"2012","primaryProfession":"actor,writer,director","knownForTitles":"tt0050083,tt0065329,tt0074042,tt0055895","kind":"profile_collision"}, + {"nconst":"nm0001433","primaryName":"Yaphet Kotto","birthYear":"1939","deathYear":"2021","primaryProfession":"actor,writer,director","knownForTitles":"tt0078748,tt0070328,tt0095631,tt0106028","kind":"profile_collision"}, + {"nconst":"nm0001492","primaryName":"Kyle MacLachlan","birthYear":"1959","deathYear":"\\N","primaryProfession":"actor,director,producer","knownForTitles":"tt4093826,tt0087182,tt1780441,tt0090756","kind":"profile_collision"}, + {"nconst":"nm0001519","primaryName":"Ian McDiarmid","birthYear":"1944","deathYear":"\\N","primaryProfession":"actor,archive_footage,archive_sound","knownForTitles":"tt0121766,tt0086190,tt0120915,tt2527338","kind":"profile_collision"}, + {"nconst":"nm0001526","primaryName":"Patrick McGoohan","birthYear":"1928","deathYear":"2009","primaryProfession":"actor,producer,director","knownForTitles":"tt0061287,tt0112573,tt0117913,tt0081455","kind":"profile_collision"}, + {"nconst":"nm0001527","primaryName":"Elizabeth McGovern","birthYear":"1961","deathYear":"\\N","primaryProfession":"actress,producer,writer","knownForTitles":"tt0081283,tt6398184,tt0099731,tt0082970","kind":"profile_collision"}, + {"nconst":"nm0001536","primaryName":"Toshirō Mifune","birthYear":"1920","deathYear":"1997","primaryProfession":"actor,producer,director","knownForTitles":"tt0055630,tt0047478,tt0050613,tt0057565","kind":"profile_collision"}, + {"nconst":"nm0001549","primaryName":"Rita Moreno","birthYear":"1931","deathYear":"\\N","primaryProfession":"actress,producer,soundtrack","knownForTitles":"tt0055614,tt3581652,tt0049408,tt0118421","kind":"profile_collision"}, + {"nconst":"nm0001590","primaryName":"Chazz Palminteri","birthYear":"1946","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt0114814,tt0106489,tt0109348,tt0473488","kind":"profile_collision"}, + {"nconst":"nm0001592","primaryName":"Joe Pantoliano","birthYear":"1951","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt0209144,tt0115736,tt0106977,tt0133093","kind":"profile_collision"}, + {"nconst":"nm0001605","primaryName":"Amanda Peet","birthYear":"1972","deathYear":"\\N","primaryProfession":"actress,writer,producer","knownForTitles":"tt1190080,tt0391304,tt0190138,tt0309698","kind":"profile_collision"}, + {"nconst":"nm0001608","primaryName":"Barry Pepper","birthYear":"1970","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt0431377,tt1403865,tt0120689,tt8364368","kind":"profile_collision"}, + {"nconst":"nm0001634","primaryName":"CCH Pounder","birthYear":"1952","deathYear":"\\N","primaryProfession":"actress,producer,archive_footage","knownForTitles":"tt0286486,tt0499549,tt1538403,tt0107978","kind":"profile_collision"}, + {"nconst":"nm0001707","primaryName":"Paul Schrader","birthYear":"1946","deathYear":"\\N","primaryProfession":"writer,director,producer","knownForTitles":"tt6053438,tt11196036,tt0118564,tt0075314","kind":"profile_collision"}, + {"nconst":"nm0001725","primaryName":"Omar Sharif","birthYear":"1932","deathYear":"2015","primaryProfession":"actor,writer,producer","knownForTitles":"tt0056172,tt0059113,tt0329388,tt0062994","kind":"profile_collision"}, + {"nconst":"nm0001741","primaryName":"Bryan Singer","birthYear":"1965","deathYear":"\\N","primaryProfession":"producer,director,actor","knownForTitles":"tt0120903,tt0114814,tt1877832,tt0348150","kind":"profile_collision"}, + {"nconst":"nm0001767","primaryName":"Imelda Staunton","birthYear":"1956","deathYear":"\\N","primaryProfession":"actress,soundtrack,archive_footage","knownForTitles":"tt0383694,tt0373889,tt3169706,tt0138097","kind":"profile_collision"}, + {"nconst":"nm0001786","primaryName":"George Takei","birthYear":"1937","deathYear":"\\N","primaryProfession":"actor,miscellaneous,writer","knownForTitles":"tt0060028,tt0102975,tt0084726,tt0088170","kind":"profile_collision"}, + {"nconst":"nm0001804","primaryName":"Stanley Tucci","birthYear":"1960","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt0115678,tt1895587,tt0380510,tt0458339","kind":"profile_collision"}, + {"nconst":"nm0001806","primaryName":"John Turturro","birthYear":"1957","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt2258345,tt0101410,tt1399103,tt0190590","kind":"profile_collision"}, + {"nconst":"nm0001814","primaryName":"Gus Van Sant","birthYear":"1952","deathYear":"\\N","primaryProfession":"director,producer,actor","knownForTitles":"tt0363589,tt0097240,tt0842929,tt0102494","kind":"profile_collision"}, + {"nconst":"nm0001832","primaryName":"Sam Waterston","birthYear":"1940","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt0087553,tt0071577,tt0098844,tt0111127","kind":"profile_collision"}, + {"nconst":"nm0001837","primaryName":"Peter Weir","birthYear":"1944","deathYear":"\\N","primaryProfession":"director,writer,producer","knownForTitles":"tt0311113,tt1023114,tt0090329,tt0097165","kind":"profile_collision"}, + {"nconst":"nm0001844","primaryName":"Frank Whaley","birthYear":"1963","deathYear":"\\N","primaryProfession":"actor,director,writer","knownForTitles":"tt0115759,tt0110912,tt0452702,tt0101761","kind":"profile_collision"}, + {"nconst":"nm0001856","primaryName":"Oprah Winfrey","birthYear":"1954","deathYear":"\\N","primaryProfession":"producer,actress,miscellaneous","knownForTitles":"tt0090493,tt1327773,tt0088939,tt1620680","kind":"profile_collision"}, + {"nconst":"nm0001938","primaryName":"Richard Belzer","birthYear":"1944","deathYear":"2023","primaryProfession":"actor,writer,producer","knownForTitles":"tt0203259,tt0106028,tt0086250,tt0120841","kind":"profile_collision"}, + {"nconst":"nm0001967","primaryName":"Peter Boyle","birthYear":"1935","deathYear":"2006","primaryProfession":"actor,miscellaneous,soundtrack","knownForTitles":"tt0115167,tt0072431,tt0111143,tt0075314","kind":"profile_collision"}, + {"nconst":"nm0001976","primaryName":"Horst Buchholz","birthYear":"1933","deathYear":"2003","primaryProfession":"actor,miscellaneous,archive_footage","knownForTitles":"tt0118799,tt0054047,tt0055256,tt0054866","kind":"profile_collision"}, + {"nconst":"nm0002009","primaryName":"Arthur C. Clarke","birthYear":"1917","deathYear":"2008","primaryProfession":"writer,actor,art_department","knownForTitles":"tt0062622,tt0086837,tt0134933,tt6076524","kind":"profile_collision"}, + {"nconst":"nm0002017","primaryName":"Richard Conte","birthYear":"1910","deathYear":"1975","primaryProfession":"actor,director,soundtrack","knownForTitles":"tt0068646,tt0041958,tt0040202,tt0041487","kind":"profile_collision"}, + {"nconst":"nm0002073","primaryName":"Lou Ferrigno","birthYear":"1951","deathYear":"\\N","primaryProfession":"actor,producer,miscellaneous","knownForTitles":"tt0077031,tt0800080,tt0286716,tt0085672","kind":"profile_collision"}, + {"nconst":"nm0002076","primaryName":"Jason Flemyng","birthYear":"1966","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt0421715,tt0120735,tt0118956,tt0486655","kind":"profile_collision"}, + {"nconst":"nm0002078","primaryName":"Frederic Forrest","birthYear":"1936","deathYear":"2023","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0078788,tt0071360,tt0106856,tt0096639","kind":"profile_collision"}, + {"nconst":"nm0002104","primaryName":"Paulette Goddard","birthYear":"1910","deathYear":"1990","primaryProfession":"actress,producer,soundtrack","knownForTitles":"tt0032520,tt0027977,tt0033722,tt0037849","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0002128","primaryName":"Tess Harper","birthYear":"1950","deathYear":"\\N","primaryProfession":"actress,archive_footage","knownForTitles":"tt0477348,tt0086423,tt0090886,tt0119395","kind":"profile_collision"}, + {"nconst":"nm0002326","primaryName":"Al Sapienza","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,producer,stunts","knownForTitles":"tt0141842,tt1856010,tt1632701,tt9288030","kind":"profile_collision"}, + {"nconst":"nm0002364","primaryName":"Dante Basco","birthYear":"1975","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt0102057,tt0179116,tt0446046,tt2139965","kind":"profile_collision"}, + {"nconst":"nm0002369","primaryName":"John Williams","birthYear":"1903","deathYear":"1983","primaryProfession":"actor,archive_footage","knownForTitles":"tt0046912,tt0047437,tt0051201,tt0048728","kind":"profile_collision"}, + {"nconst":"nm0002728","primaryName":"Juan José Campanella","birthYear":"1959","deathYear":"\\N","primaryProfession":"writer,director,producer","knownForTitles":"tt1305806,tt1994702,tt8453986,tt0210843","kind":"profile_collision"}, + {"nconst":"nm0003697","primaryName":"Florian Henckel von Donnersmarck","birthYear":"1973","deathYear":"\\N","primaryProfession":"director,writer,producer","knownForTitles":"tt0405094,tt5311542,tt1243957,tt0233589","kind":"profile_collision"}, + {"nconst":"nm0004111","primaryName":"Mark Frost","birthYear":"1953","deathYear":"\\N","primaryProfession":"writer,producer,director","knownForTitles":"tt0120667,tt0081873,tt0098936,tt4093826","kind":"profile_collision"}, + {"nconst":"nm0004266","primaryName":"Anne Hathaway","birthYear":"1982","deathYear":"\\N","primaryProfession":"actress,producer,miscellaneous","knownForTitles":"tt1707386,tt1084950,tt1345836,tt0816692","kind":"profile_collision"}, + {"nconst":"nm0004423","primaryName":"Gerry Robert Byrne","birthYear":"\\N","deathYear":"\\N","primaryProfession":"production_manager,producer,actor","knownForTitles":"tt0217505,tt0388795,tt6866266,tt1327773","kind":"profile_collision"}, + {"nconst":"nm0004626","primaryName":"Kareena Kapoor","birthYear":"1980","deathYear":"\\N","primaryProfession":"actress,music_department,make_up_department","knownForTitles":"tt1093370,tt1187043,tt0488414,tt1562871","kind":"profile_collision"}, + {"nconst":"nm0004743","primaryName":"Gil Bellows","birthYear":"1967","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt4687882,tt0111161,tt0110395,tt8594276","kind":"profile_collision"}, + {"nconst":"nm0004988","primaryName":"Colin Hanks","birthYear":"1977","deathYear":"\\N","primaryProfession":"actor,director,producer","knownForTitles":"tt0360717,tt0273923,tt0852713,tt0880578","kind":"profile_collision"}, + {"nconst":"nm0005002","primaryName":"Isaac Hayes","birthYear":"1942","deathYear":"2008","primaryProfession":"music_artist,actor,music_department","knownForTitles":"tt0082340,tt0158983,tt0184858,tt0067741","kind":"profile_collision"}, + {"nconst":"nm0005017","primaryName":"Katie Holmes","birthYear":"1978","deathYear":"\\N","primaryProfession":"actress,producer,director","knownForTitles":"tt0134619,tt0372784,tt0810913,tt0118300","kind":"profile_collision"}, + {"nconst":"nm0005078","primaryName":"Stacy Keach","birthYear":"1941","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt1821549,tt0068575,tt0116225,tt1194173","kind":"profile_collision"}, + {"nconst":"nm0005082","primaryName":"David E. Kelley","birthYear":"1956","deathYear":"\\N","primaryProfession":"writer,producer,miscellaneous","knownForTitles":"tt0118437,tt3920596,tt0090466,tt0118254","kind":"profile_collision"}, + {"nconst":"nm0005132","primaryName":"Heath Ledger","birthYear":"1979","deathYear":"2008","primaryProfession":"actor,director,cinematographer","knownForTitles":"tt0388795,tt0468569,tt0183790,tt0355295","kind":"profile_collision"}, + {"nconst":"nm0005200","primaryName":"Paul McCartney","birthYear":"1942","deathYear":"\\N","primaryProfession":"music_artist,composer,actor","knownForTitles":"tt0058182,tt0078239,tt0063823,tt9735318","kind":"profile_collision"}, + {"nconst":"nm0005222","primaryName":"Sam Mendes","birthYear":"1965","deathYear":"\\N","primaryProfession":"producer,director,miscellaneous","knownForTitles":"tt8579674,tt14402146,tt0257044,tt1074638","kind":"profile_collision"}, + {"nconst":"nm0005305","primaryName":"Piper Perabo","birthYear":"1976","deathYear":"\\N","primaryProfession":"actor,actress,producer","knownForTitles":"tt0200550,tt4236770,tt0482571,tt1495708","kind":"profile_collision"}, + {"nconst":"nm0005311","primaryName":"Busy Philipps","birthYear":"1979","deathYear":"\\N","primaryProfession":"actress,producer,director","knownForTitles":"tt1001508,tt0866439,tt0381707,tt1441109","kind":"profile_collision"}, + {"nconst":"nm0005370","primaryName":"Kathleen Robertson","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,writer,producer","knownForTitles":"tt9288030,tt11343686,tt1833285,tt2402061","kind":"profile_collision"}, + {"nconst":"nm0005458","primaryName":"Jason Statham","birthYear":"1967","deathYear":"\\N","primaryProfession":"actor,producer,stunts","knownForTitles":"tt0479884,tt2820852,tt3079380,tt4779682","kind":"profile_collision"}, + {"nconst":"nm0005531","primaryName":"Donnie Wahlberg","birthYear":"1969","deathYear":"\\N","primaryProfession":"actor,producer,composer","knownForTitles":"tt0432348,tt0117438,tt0455760,tt0167404","kind":"profile_collision"}, + {"nconst":"nm0005606","primaryName":"Maurice LaMarche","birthYear":"1958","deathYear":"\\N","primaryProfession":"actor,producer,sound_department","knownForTitles":"tt0149460,tt0112123,tt2948356,tt0096697","kind":"profile_collision"}, + {"nconst":"nm0006168","primaryName":"John Lennon","birthYear":"1940","deathYear":"1980","primaryProfession":"music_artist,composer,actor","knownForTitles":"tt0058182,tt0078239,tt0063823,tt0059260","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0006471","primaryName":"Elsa Lanchester","birthYear":"1902","deathYear":"1986","primaryProfession":"actress,soundtrack,archive_footage","knownForTitles":"tt0051201,tt0074937,tt0026138,tt0058331","kind":"profile_collision"}, + {"nconst":"nm0008609","primaryName":"Johnny Aber","birthYear":"1916","deathYear":"1999","primaryProfession":"actor,archive_footage","knownForTitles":"tt0235076,tt0234861,tt0234751,tt0234787","kind":"profile_collision"}, + {"nconst":"nm0012524","primaryName":"Frank Adu","birthYear":"1937","deathYear":"2018","primaryProfession":"actor,archive_footage","knownForTitles":"tt0073312,tt0075314,tt0126916,tt0104797","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0016641","primaryName":"Fritz Alberti","birthYear":"1877","deathYear":"1954","primaryProfession":"actor,archive_footage","knownForTitles":"tt0015175,tt0017438,tt0015174,tt0022930","kind":"profile_collision"}, + {"nconst":"nm0018685","primaryName":"Richard Alexander","birthYear":"1902","deathYear":"1989","primaryProfession":"actor,archive_footage","knownForTitles":"tt0020629,tt0027977,tt0027623,tt0019532","kind":"profile_collision"}, + {"nconst":"nm0024404","primaryName":"Lauren Ambrose","birthYear":"1978","deathYear":"\\N","primaryProfession":"actress,producer,soundtrack","knownForTitles":"tt0248654,tt0386117,tt0127723,tt0206226","kind":"profile_collision"}, + {"nconst":"nm0026364","primaryName":"Anthony Anderson","birthYear":"1970","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt0407887,tt0410097,tt0257568,tt0418279","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0029508","primaryName":"June Angela","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,miscellaneous,music_department","knownForTitles":"tt0066651,tt3551096,tt0097814,tt0108757","kind":"profile_collision"}, + {"nconst":"nm0029962","primaryName":"Álex Angulo","birthYear":"1953","deathYear":"2014","primaryProfession":"actor,producer,archive_footage","knownForTitles":"tt0457430,tt0112922,tt0118819,tt0168080","kind":"profile_collision"}, + {"nconst":"nm0034877","primaryName":"Jonathan Aris","birthYear":"1971","deathYear":"\\N","primaryProfession":"actor,archive_footage","knownForTitles":"tt32061880,tt6257970,tt8368406,tt1475582","kind":"profile_collision"}, + {"nconst":"nm0039162","primaryName":"Shawn Ashmore","birthYear":"1979","deathYear":"\\N","primaryProfession":"actor,producer,archive_footage","knownForTitles":"tt0290334,tt1323045,tt0376994,tt0120903","kind":"profile_collision"}, + {"nconst":"nm0041003","primaryName":"David Attenborough","birthYear":"1926","deathYear":"\\N","primaryProfession":"writer,producer,director","knownForTitles":"tt6769208,tt0795176,tt2092588,tt0135095","kind":"profile_collision"}, + {"nconst":"nm0047962","primaryName":"Chieko Baishō","birthYear":"1941","deathYear":"\\N","primaryProfession":"actress,music_department,soundtrack","knownForTitles":"tt0347149,tt0081288,tt0073498,tt0066190","kind":"profile_collision"}, + {"nconst":"nm0048075","primaryName":"Manoj Bajpayee","birthYear":"1969","deathYear":"\\N","primaryProfession":"actor,producer,music_department","knownForTitles":"tt1954470,tt0195231,tt0289845,tt23864864","kind":"profile_collision"}, + {"nconst":"nm0048468","primaryName":"George Baker","birthYear":"1931","deathYear":"2011","primaryProfession":"actor,writer,production_department","knownForTitles":"tt0064757,tt0076752,tt0074006,tt0048611","kind":"profile_collision"}, + {"nconst":"nm0050332","primaryName":"Alan Ball","birthYear":"1957","deathYear":"\\N","primaryProfession":"producer,writer,miscellaneous","knownForTitles":"tt0169547,tt0248654,tt0844441,tt11327514","kind":"profile_collision"}, + {"nconst":"nm0051257","primaryName":"Kirk Baltz","birthYear":"1959","deathYear":"\\N","primaryProfession":"actor,producer,miscellaneous","knownForTitles":"tt0110632,tt0118798,tt0119094,tt0105236","kind":"profile_collision"}, + {"nconst":"nm0051397","primaryName":"Jamie Bamber","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,producer,archive_footage","knownForTitles":"tt0407362,tt1166893,tt5269594,tt1492179","kind":"profile_collision"}, + {"nconst":"nm0063527","primaryName":"Brian Beacock","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,miscellaneous,producer","knownForTitles":"tt3528758,tt3178442,tt0218864,tt0328790","kind":"profile_collision"}, + {"nconst":"nm0064235","primaryName":"Robert Beatty","birthYear":"1909","deathYear":"1992","primaryProfession":"actor,archive_footage","knownForTitles":"tt0065207,tt0075066,tt0062622,tt0094074","kind":"profile_collision"}, + {"nconst":"nm0064769","primaryName":"Jim Beaver","birthYear":"1950","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt0348914,tt0460681,tt0903747,tt1190634","kind":"profile_collision"}, + {"nconst":"nm0065874","primaryName":"Andy Beckwith","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,archive_footage","knownForTitles":"tt1355631,tt0944947,tt5691024,tt0208092","kind":"profile_collision"}, + {"nconst":"nm0071818","primaryName":"Jeff Bennett","birthYear":"1962","deathYear":"\\N","primaryProfession":"actor,music_department,miscellaneous","knownForTitles":"tt0461770,tt0118360,tt0318155,tt0892700","kind":"profile_collision"}, + {"nconst":"nm0074834","primaryName":"Mary Kay Bergman","birthYear":"1961","deathYear":"1999","primaryProfession":"actress,miscellaneous,producer","knownForTitles":"tt0158983,tt0101414,tt0119282,tt0120762","kind":"profile_collision"}, + {"nconst":"nm0075588","primaryName":"Jeannie Berlin","birthYear":"1949","deathYear":"\\N","primaryProfession":"actress,writer,director","knownForTitles":"tt0068687,tt0466893,tt14208870,tt1791528","kind":"profile_collision"}, + {"nconst":"nm0077720","primaryName":"Michael Berryman","birthYear":"1948","deathYear":"\\N","primaryProfession":"actor,producer,transportation_department","knownForTitles":"tt0077681,tt0090305,tt0395584,tt0073486","kind":"profile_collision"}, + {"nconst":"nm0080049","primaryName":"Yasiin Bey","birthYear":"1973","deathYear":"\\N","primaryProfession":"actor,producer,music_department","knownForTitles":"tt0317740,tt0371724,tt0361127,tt0450232","kind":"profile_collision"}, + {"nconst":"nm0080232","primaryName":"Nitish Bharadwaj","birthYear":"1963","deathYear":"\\N","primaryProfession":"actor,director,writer","knownForTitles":"tt0158417,tt0389673,tt3133538,tt0397214","kind":"profile_collision"}, + {"nconst":"nm0081449","primaryName":"Dragan Bjelogrlic","birthYear":"1963","deathYear":"\\N","primaryProfession":"director,actor,producer","knownForTitles":"tt20365920,tt8737152,tt6108262,tt1634013","kind":"profile_collision"}, + {"nconst":"nm0082450","primaryName":"Paul Feig","birthYear":"1962","deathYear":"\\N","primaryProfession":"producer,actor,director","knownForTitles":"tt27543632,tt2404463,tt0193676,tt0110006","kind":"profile_collision"}, + {"nconst":"nm0083655","primaryName":"Gil Birmingham","birthYear":"1953","deathYear":"\\N","primaryProfession":"actor,miscellaneous,archive_footage","knownForTitles":"tt5362988,tt1325004,tt1099212,tt1324999","kind":"profile_collision"}, + {"nconst":"nm0090008","primaryName":"Stanley Blystone","birthYear":"1894","deathYear":"1956","primaryProfession":"actor,archive_footage","knownForTitles":"tt0027977,tt0040333,tt0036381,tt0023013","kind":"profile_collision"}, + {"nconst":"nm0090386","primaryName":"James Bobin","birthYear":"1972","deathYear":"\\N","primaryProfession":"director,writer,producer","knownForTitles":"tt2281587,tt1204342,tt7547410,tt0863046","kind":"profile_collision"}, + {"nconst":"nm0093769","primaryName":"Fortunio Bonanova","birthYear":"1895","deathYear":"1969","primaryProfession":"actor,director,composer","knownForTitles":"tt0033467,tt0036775,tt0037979,tt0050105","kind":"profile_collision"}, + {"nconst":"nm0094435","primaryName":"Bong Joon Ho","birthYear":"1969","deathYear":"\\N","primaryProfession":"writer,producer,director","knownForTitles":"tt1706620,tt3967856,tt6751668,tt1216496","kind":"profile_collision"}, + {"nconst":"nm0095460","primaryName":"Ed Boon","birthYear":"1964","deathYear":"\\N","primaryProfession":"writer,miscellaneous,actor","knownForTitles":"tt0119707,tt0113855,tt17490712,tt0293429","kind":"profile_collision"}, + {"nconst":"nm0097504","primaryName":"Alex Borstein","birthYear":"1971","deathYear":"\\N","primaryProfession":"actress,music_department,producer","knownForTitles":"tt0182576,tt0385690,tt0307987,tt0327554","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0103495","primaryName":"James Bradshaw","birthYear":"1976","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt2701582,tt4696944,tt0808096,tt0187643","kind":"profile_collision"}, + {"nconst":"nm0104006","primaryName":"Tom Braidwood","birthYear":"1948","deathYear":"\\N","primaryProfession":"assistant_director,actor,producer","knownForTitles":"tt0120902,tt0156442,tt0443701,tt0106179","kind":"profile_collision"}, + {"nconst":"nm0105304","primaryName":"Patrick Brantseg","birthYear":"1967","deathYear":"\\N","primaryProfession":"miscellaneous,art_director,actor","knownForTitles":"tt0094517,tt0117128,tt0435555,tt0387005","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0107950","primaryName":"Jeremy Brett","birthYear":"1933","deathYear":"1995","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0058385,tt0049934,tt0095330,tt0685628","kind":"profile_collision"}, + {"nconst":"nm0110167","primaryName":"Chris Britton","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,miscellaneous","knownForTitles":"tt0319262,tt0364343,tt0335121,tt0119217","kind":"profile_collision"}, + {"nconst":"nm0110480","primaryName":"Peter Brocco","birthYear":"1903","deathYear":"1992","primaryProfession":"actor,archive_footage","knownForTitles":"tt0054331,tt0094142,tt0073486,tt0059557","kind":"profile_collision"}, + {"nconst":"nm0112459","primaryName":"Aline Brosh McKenna","birthYear":"1967","deathYear":"\\N","primaryProfession":"producer,writer,director","knownForTitles":"tt4094300,tt0458352,tt12823454,tt1126618","kind":"profile_collision"}, + {"nconst":"nm0114868","primaryName":"W. Earl Brown","birthYear":"1963","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt0348914,tt0129387,tt32268630,tt11815682","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0116897","primaryName":"Arthur Q. Bryan","birthYear":"1899","deathYear":"1959","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0032390,tt0032795,tt0047075,tt0036941","kind":"profile_collision"}, + {"nconst":"nm0122653","primaryName":"Edward Burns","birthYear":"1968","deathYear":"\\N","primaryProfession":"writer,actor,producer","knownForTitles":"tt0117628,tt0120815,tt0112585,tt0491109","kind":"profile_collision"}, + {"nconst":"nm0123404","primaryName":"Andrew Burt","birthYear":"1945","deathYear":"2018","primaryProfession":"actor,archive_footage","knownForTitles":"tt0404196,tt0094430,tt2158111,tt0068069","kind":"profile_collision"}, + {"nconst":"nm0123785","primaryName":"Ben Burtt","birthYear":"1948","deathYear":"\\N","primaryProfession":"sound_department,miscellaneous,editor","knownForTitles":"tt0910970,tt0076759,tt0120915,tt1650062","kind":"profile_collision"}, + {"nconst":"nm0126260","primaryName":"P.J. Byrne","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,writer,director","knownForTitles":"tt0993846,tt11995650,tt1190634,tt10640346","kind":"profile_collision"}, + {"nconst":"nm0130502","primaryName":"Dayton Callie","birthYear":"1946","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt0120461,tt0114682,tt1311067,tt0116828","kind":"profile_collision"}, + {"nconst":"nm0130536","primaryName":"James Callis","birthYear":"1971","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt5875444,tt0314979,tt6517102,tt0243155","kind":"profile_collision"}, + {"nconst":"nm0134666","primaryName":"Geoffrey Cantor","birthYear":"1962","deathYear":"\\N","primaryProfession":"actor,director,casting_director","knownForTitles":"tt3322312,tt0475290,tt5580146,tt7461272","kind":"profile_collision"}, + {"nconst":"nm0141953","primaryName":"T.K. Carter","birthYear":"1956","deathYear":"2026","primaryProfession":"actor,producer,miscellaneous","knownForTitles":"tt0084787,tt0421054,tt0089941,tt0081480","kind":"profile_collision"}, + {"nconst":"nm0144307","primaryName":"John Cassini","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,miscellaneous,producer","knownForTitles":"tt0402910,tt0114369,tt0208988,tt0315323","kind":"profile_collision"}, + {"nconst":"nm0144657","primaryName":"Dan Castellaneta","birthYear":"1957","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt0096697,tt0462538,tt0117705,tt0386789","kind":"profile_collision"}, + {"nconst":"nm0148437","primaryName":"Vincenzo Cerami","birthYear":"1940","deathYear":"2013","primaryProfession":"writer,actor,assistant_director","knownForTitles":"tt0118799,tt0419198,tt0100389,tt0102164","kind":"profile_collision"}, + {"nconst":"nm0153068","primaryName":"Jonathan Fahn","birthYear":"1965","deathYear":"\\N","primaryProfession":"actor,director,producer","knownForTitles":"tt0301290,tt0301921,tt1130090,tt4902250","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0153740","primaryName":"David Chase","birthYear":"1945","deathYear":"\\N","primaryProfession":"producer,writer,miscellaneous","knownForTitles":"tt0141842,tt0071042,tt0101124,tt0094414","kind":"profile_collision"}, + {"nconst":"nm0156928","primaryName":"Minoru Chiaki","birthYear":"1917","deathYear":"1999","primaryProfession":"actor,archive_footage","knownForTitles":"tt0047478,tt0042876,tt0051808,tt0050613","kind":"profile_collision"}, + {"nconst":"nm0156940","primaryName":"Dominic Chianese","birthYear":"1931","deathYear":"\\N","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0141842,tt0071562,tt0078718,tt0072890","kind":"profile_collision"}, + {"nconst":"nm0157058","primaryName":"Giulio Chiari","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt0040522","kind":"profile_collision"}, + {"nconst":"nm0157120","primaryName":"Isshin Chiba","birthYear":"1968","deathYear":"\\N","primaryProfession":"actor,archive_footage,archive_sound","knownForTitles":"tt2288938,tt0213338,tt0361140,tt2084868","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0165087","primaryName":"Jeremy Clarkson","birthYear":"1960","deathYear":"\\N","primaryProfession":"writer,producer,actor","knownForTitles":"tt1628033,tt0857190,tt0852863,tt0407290","kind":"profile_collision"}, + {"nconst":"nm0165412","primaryName":"James Clavell","birthYear":"1921","deathYear":"1994","primaryProfession":"writer,director,producer","knownForTitles":"tt0057115,tt0051622,tt0062376,tt0065969","kind":"profile_collision"}, + {"nconst":"nm0166359","primaryName":"David Clennon","birthYear":"1943","deathYear":"\\N","primaryProfession":"actor,archive_footage","knownForTitles":"tt0084335,tt0365737,tt0084787,tt2267998","kind":"profile_collision"}, + {"nconst":"nm0167388","primaryName":"François Cluzet","birthYear":"1955","deathYear":"\\N","primaryProfession":"actor,miscellaneous,soundtrack","knownForTitles":"tt1675434,tt0362225,tt0113117,tt0109731","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0175916","primaryName":"Paddy Considine","birthYear":"1973","deathYear":"\\N","primaryProfession":"actor,writer,director","knownForTitles":"tt0419677,tt1213663,tt0298845,tt1204340","kind":"profile_collision"}, + {"nconst":"nm0179289","primaryName":"Ellen Corby","birthYear":"1911","deathYear":"1999","primaryProfession":"actress,writer,script_department","knownForTitles":"tt0068149,tt0052357,tt0040458,tt0042296","kind":"profile_collision"}, + {"nconst":"nm0179819","primaryName":"Wendell Corey","birthYear":"1914","deathYear":"1968","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0047396,tt0061203,tt0060074,tt0040765","kind":"profile_collision"}, + {"nconst":"nm0185354","primaryName":"Brendan Coyle","birthYear":"1963","deathYear":"\\N","primaryProfession":"actor,producer,archive_footage","knownForTitles":"tt1606375,tt6398184,tt2674426,tt1077744","kind":"profile_collision"}, + {"nconst":"nm0185431","primaryName":"Jonny Coyne","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,archive_footage","knownForTitles":"tt30825738,tt8111088,tt10514222,tt16431870","kind":"profile_collision"}, + {"nconst":"nm0189144","primaryName":"David Cross","birthYear":"1964","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt0367279,tt0338013,tt1302011,tt1001526","kind":"profile_collision"}, + {"nconst":"nm0193485","primaryName":"Richard Curtis","birthYear":"1956","deathYear":"\\N","primaryProfession":"writer,producer,director","knownForTitles":"tt0314331,tt0109831,tt2194499,tt1131729","kind":"profile_collision"}, + {"nconst":"nm0200452","primaryName":"Paul Dano","birthYear":"1984","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt0469494,tt0449059,tt2024544,tt0903657","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0201669","primaryName":"Jean-Pierre Darroussin","birthYear":"1953","deathYear":"\\N","primaryProfession":"actor,director,writer","knownForTitles":"tt1508675,tt0118015,tt0474312,tt0319130","kind":"profile_collision"}, + {"nconst":"nm0201857","primaryName":"Ricardo Darín","birthYear":"1957","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt0247586,tt1305806,tt3011894,tt15301048","kind":"profile_collision"}, + {"nconst":"nm0203801","primaryName":"Karl Davies","birthYear":"1982","deathYear":"\\N","primaryProfession":"actor,producer,archive_footage","knownForTitles":"tt3428912,tt7366338,tt0944947,tt8697870","kind":"profile_collision"}, + {"nconst":"nm0205626","primaryName":"Viola Davis","birthYear":"1965","deathYear":"\\N","primaryProfession":"actress,producer,soundtrack","knownForTitles":"tt1454029,tt2671706,tt0918927,tt8093700","kind":"profile_collision"}, + {"nconst":"nm0206257","primaryName":"Rosario Dawson","birthYear":"1979","deathYear":"\\N","primaryProfession":"actress,producer,director","knownForTitles":"tt0294870,tt4116284,tt0401792,tt0477080","kind":"profile_collision"}, + {"nconst":"nm0209553","primaryName":"María Antonieta de las Nieves","birthYear":"1946","deathYear":"\\N","primaryProfession":"actress,miscellaneous,writer","knownForTitles":"tt0229890,tt0021884,tt0229889,tt0032138","kind":"profile_collision"}, + {"nconst":"nm0213354","primaryName":"Jamel Debbouze","birthYear":"1975","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt0211915,tt0473753,tt0250223,tt0444182","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0214354","primaryName":"Deepika Chikhalia","birthYear":"1965","deathYear":"\\N","primaryProfession":"actress,producer,music_department","knownForTitles":"tt0294139,tt0268093,tt0390792,tt0363967","kind":"profile_collision"}, + {"nconst":"nm0215916","primaryName":"Barry Del Sherman","birthYear":"1962","deathYear":"\\N","primaryProfession":"actor,archive_footage","knownForTitles":"tt0169547,tt0116629,tt0120241,tt0113690","kind":"profile_collision"}, + {"nconst":"nm0216507","primaryName":"Brian Delate","birthYear":"1949","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt0111161,tt0120382,tt0294870,tt0252299","kind":"profile_collision"}, + {"nconst":"nm0219465","primaryName":"Hugh Dennis","birthYear":"1962","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt0233084,tt2382320,tt0463827,tt5687612","kind":"profile_collision"}, + {"nconst":"nm0226813","primaryName":"Garret Dillahunt","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt2024544,tt0348914,tt0477348,tt1615919","kind":"profile_collision"}, + {"nconst":"nm0227759","primaryName":"Peter Dinklage","birthYear":"1969","deathYear":"\\N","primaryProfession":"actor,producer,stunts","knownForTitles":"tt0944947,tt0340377,tt1877832,tt5027774","kind":"profile_collision"}, + {"nconst":"nm0230826","primaryName":"Monica Dolan","birthYear":"1969","deathYear":"\\N","primaryProfession":"actress,soundtrack","knownForTitles":"tt2057392,tt3169706,tt2023690,tt3294200","kind":"profile_collision"}, + {"nconst":"nm0235103","primaryName":"Jason Douglas","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,miscellaneous,archive_footage","knownForTitles":"tt1520211,tt5016504,tt14614892,tt0477348","kind":"profile_collision"}, + {"nconst":"nm0235261","primaryName":"Terri Douglas","birthYear":"\\N","deathYear":"\\N","primaryProfession":"casting_department,actress,miscellaneous","knownForTitles":"tt4520988,tt22022452,tt29355505,tt30825738","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0235417","primaryName":"Tony Doupe","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt1862079,tt28494270,tt15133756,tt2305051","kind":"profile_collision"}, + {"nconst":"nm0241121","primaryName":"Jean Dujardin","birthYear":"1972","deathYear":"\\N","primaryProfession":"actor,director,writer","knownForTitles":"tt1655442,tt0464913,tt0993846,tt0412535","kind":"profile_collision"}, + {"nconst":"nm0244357","primaryName":"Lawrence Durrell","birthYear":"1912","deathYear":"1990","primaryProfession":"writer,actor,archive_footage","knownForTitles":"tt0060568,tt0064526,tt0054513,tt0861749","kind":"profile_collision"}, + {"nconst":"nm0252230","primaryName":"Chiwetel Ejiofor","birthYear":"1977","deathYear":"\\N","primaryProfession":"actor,director,writer","knownForTitles":"tt0301199,tt2024544,tt7533152,tt0379786","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0252238","primaryName":"Carmen Ejogo","birthYear":"1973","deathYear":"\\N","primaryProfession":"actress,producer,soundtrack","knownForTitles":"tt1020072,tt4695012,tt2975578,tt3183660","kind":"profile_collision"}, + {"nconst":"nm0263006","primaryName":"Mal Evans","birthYear":"1935","deathYear":"1976","primaryProfession":"miscellaneous,music_department,actor","knownForTitles":"tt0071760,tt0065976,tt0068307,tt9735318","kind":"profile_collision"}, + {"nconst":"nm0268380","primaryName":"Peter Farrelly","birthYear":"1956","deathYear":"\\N","primaryProfession":"producer,writer,director","knownForTitles":"tt6966692,tt0129387,tt0109686,tt1333125","kind":"profile_collision"}, + {"nconst":"nm0269463","primaryName":"Jon Favreau","birthYear":"1966","deathYear":"\\N","primaryProfession":"producer,actor,writer","knownForTitles":"tt2883512,tt0371746,tt0117802,tt4154796","kind":"profile_collision"}, + {"nconst":"nm0270625","primaryName":"JJ Feild","birthYear":"1978","deathYear":"\\N","primaryProfession":"actor,producer,soundtrack","knownForTitles":"tt1985019,tt0458339,tt1020558,tt6133130","kind":"profile_collision"}, + {"nconst":"nm0271965","primaryName":"Dave Fennoy","birthYear":"1952","deathYear":"\\N","primaryProfession":"actor,miscellaneous,producer","knownForTitles":"tt2006890,tt1037854,tt0462423,tt5785964","kind":"profile_collision"}, + {"nconst":"nm0272581","primaryName":"Rebecca Ferguson","birthYear":"1983","deathYear":"\\N","primaryProfession":"actress,producer,soundtrack","knownForTitles":"tt1485796,tt2381249,tt1160419,tt4912910","kind":"profile_collision"}, + {"nconst":"nm0283945","primaryName":"Dan Fogler","birthYear":"1976","deathYear":"\\N","primaryProfession":"actor,director,writer","knownForTitles":"tt3183660,tt0489049,tt0452625,tt0441773","kind":"profile_collision"}, + {"nconst":"nm0288144","primaryName":"Alastair Fothergill","birthYear":"1960","deathYear":"\\N","primaryProfession":"producer,director,writer","knownForTitles":"tt0296310,tt9253866,tt0795176,tt11989890","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0290117","primaryName":"Clive Francis","birthYear":"1946","deathYear":"\\N","primaryProfession":"actor,art_department,visual_effects","knownForTitles":"tt0066921,tt5431890,tt2473794,tt1212428","kind":"profile_collision"}, + {"nconst":"nm0290556","primaryName":"James Franco","birthYear":"1978","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt3521126,tt2101441,tt1542344,tt1245492","kind":"profile_collision"}, + {"nconst":"nm0291205","primaryName":"David Frankel","birthYear":"1959","deathYear":"\\N","primaryProfession":"director,producer,writer","knownForTitles":"tt0458352,tt0185906,tt1196956,tt0096566","kind":"profile_collision"}, + {"nconst":"nm0291569","primaryName":"Vincent Franklin","birthYear":"1966","deathYear":"\\N","primaryProfession":"actor,writer,soundtrack","knownForTitles":"tt0258463,tt0120681,tt0810784,tt0151568","kind":"profile_collision"}, + {"nconst":"nm0297054","primaryName":"Gustav Fröhlich","birthYear":"1902","deathYear":"1987","primaryProfession":"actor,director,writer","knownForTitles":"tt0017136,tt0904098,tt0048598,tt0041150","kind":"profile_collision"}, + {"nconst":"nm0301348","primaryName":"Richard Gaines","birthYear":"1904","deathYear":"1975","primaryProfession":"actor","knownForTitles":"tt0036775,tt0043338,tt0036172,tt0037671","kind":"profile_collision"}, + {"nconst":"nm0302330","primaryName":"John Gallagher Jr.","birthYear":"1984","deathYear":"\\N","primaryProfession":"actor,producer,soundtrack","knownForTitles":"tt1179933,tt2370248,tt5022702,tt1082807","kind":"profile_collision"}, + {"nconst":"nm0305558","primaryName":"Gael García Bernal","birthYear":"1978","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt3502172,tt0318462,tt0245574,tt0245712","kind":"profile_collision"}, + {"nconst":"nm0306786","primaryName":"Reginald Gardiner","birthYear":"1903","deathYear":"1980","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0032553,tt0037595,tt0033874,tt0028757","kind":"profile_collision"}, + {"nconst":"nm0309379","primaryName":"Gregory Gast","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,editorial_department,camera_department","knownForTitles":"tt13833978,tt14111734,tt0338348,tt0228528","kind":"profile_collision"}, + {"nconst":"nm0309693","primaryName":"Mark Gatiss","birthYear":"1966","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt0184135,tt1475582,tt0371724,tt9603212","kind":"profile_collision"}, + {"nconst":"nm0309945","primaryName":"Maxim Gaudette","birthYear":"1974","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt1255953,tt1194238,tt2486148,tt4881276","kind":"profile_collision"}, + {"nconst":"nm0311476","primaryName":"Martina Gedeck","birthYear":"1961","deathYear":"\\N","primaryProfession":"actress,miscellaneous,soundtrack","knownForTitles":"tt0405094,tt0765432,tt0246772,tt0343737","kind":"profile_collision"}, + {"nconst":"nm0320721","primaryName":"Rémy Girard","birthYear":"1950","deathYear":"\\N","primaryProfession":"actor,miscellaneous,soundtrack","knownForTitles":"tt0338135,tt1255953,tt0090985,tt0097635","kind":"profile_collision"}, + {"nconst":"nm0320812","primaryName":"Sara Giraudeau","birthYear":"1985","deathYear":"\\N","primaryProfession":"actress,director,cinematographer","knownForTitles":"tt4063800,tt6805302,tt21028846,tt3854802","kind":"profile_collision"}, + {"nconst":"nm0321294","primaryName":"Aldo Giuffrè","birthYear":"1924","deathYear":"2010","primaryProfession":"actor,miscellaneous,soundtrack","knownForTitles":"tt0060196,tt0063480,tt0066268,tt0057036","kind":"profile_collision"}, + {"nconst":"nm0324658","primaryName":"Walton Goggins","birthYear":"1971","deathYear":"\\N","primaryProfession":"actor,producer,soundtrack","knownForTitles":"tt1489428,tt3460252,tt1424381,tt1853728","kind":"profile_collision"}, + {"nconst":"nm0328751","primaryName":"Caroline Goodall","birthYear":"1959","deathYear":"\\N","primaryProfession":"actress,producer,writer","knownForTitles":"tt0109635,tt0360139,tt0106582,tt0108052","kind":"profile_collision"}, + {"nconst":"nm0331516","primaryName":"Ryan Gosling","birthYear":"1980","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt1120985,tt3783958,tt0780504,tt0332280","kind":"profile_collision"}, + {"nconst":"nm0335073","primaryName":"Philip Granger","birthYear":"1964","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt26657236,tt26625693,tt11337908,tt8613988","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0338169","primaryName":"Michael Green","birthYear":"1973","deathYear":"\\N","primaryProfession":"producer,writer,miscellaneous","knownForTitles":"tt3315342,tt1856101,tt13309742,tt1133985","kind":"profile_collision"}, + {"nconst":"nm0340580","primaryName":"Harry Grey","birthYear":"1901","deathYear":"1980","primaryProfession":"writer","knownForTitles":"tt0087843,tt0055317","kind":"profile_collision"}, + {"nconst":"nm0347039","primaryName":"Robert Guillaume","birthYear":"1927","deathYear":"2017","primaryProfession":"actor,producer,director","knownForTitles":"tt0078569,tt0110357,tt0165961,tt0097722","kind":"profile_collision"}, + {"nconst":"nm0355097","primaryName":"Jackie Earle Haley","birthYear":"1961","deathYear":"\\N","primaryProfession":"actor,director,writer","knownForTitles":"tt0409459,tt1179056,tt0074174,tt1130884","kind":"profile_collision"}, + {"nconst":"nm0358452","primaryName":"Doc Hammer","birthYear":"1967","deathYear":"\\N","primaryProfession":"visual_effects,actor,animation_department","knownForTitles":"tt0417373,tt14636186,tt0102731,tt0318376","kind":"profile_collision"}, + {"nconst":"nm0364748","primaryName":"Harriet Sansom Harris","birthYear":"1955","deathYear":"\\N","primaryProfession":"actress,soundtrack,archive_footage","knownForTitles":"tt11271038,tt5776858,tt26656917,tt13652142","kind":"profile_collision"}, + {"nconst":"nm0364915","primaryName":"Julie Harris","birthYear":"1925","deathYear":"2013","primaryProfession":"actress,soundtrack,archive_footage","knownForTitles":"tt0057129,tt0048028,tt0078638,tt0044896","kind":"profile_collision"}, + {"nconst":"nm0369617","primaryName":"Jerry Hausner","birthYear":"1909","deathYear":"1993","primaryProfession":"actor,miscellaneous,writer","knownForTitles":"tt0050825,tt0048412,tt0046138,tt0041061","kind":"profile_collision"}, + {"nconst":"nm0372423","primaryName":"Mark Heap","birthYear":"1957","deathYear":"\\N","primaryProfession":"actor,writer,archive_footage","knownForTitles":"tt0486655,tt0423661,tt1077744,tt1213663","kind":"profile_collision"}, + {"nconst":"nm0375738","primaryName":"Tom Helmore","birthYear":"1904","deathYear":"1995","primaryProfession":"actor,archive_footage","knownForTitles":"tt0052357,tt0054387,tt0051074,tt1090685","kind":"profile_collision"}, + {"nconst":"nm0376540","primaryName":"Martin Henderson","birthYear":"1974","deathYear":"\\N","primaryProfession":"actor,director,assistant_director","knownForTitles":"tt0475394,tt13560574,tt0454824,tt0298130","kind":"profile_collision"}, + {"nconst":"nm0383091","primaryName":"Bokuzen Hidari","birthYear":"1894","deathYear":"1971","primaryProfession":"actor,archive_footage","knownForTitles":"tt0047478,tt0044741,tt0050330,tt0043707","kind":"profile_collision"}, + {"nconst":"nm0384211","primaryName":"Dulé Hill","birthYear":"1975","deathYear":"\\N","primaryProfession":"actor,producer,soundtrack","knownForTitles":"tt0491738,tt0200276,tt1632701,tt2891574","kind":"profile_collision"}, + {"nconst":"nm0402898","primaryName":"Kathryn Hunter","birthYear":"1957","deathYear":"\\N","primaryProfession":"actress,producer,miscellaneous","knownForTitles":"tt10095582,tt14230458,tt0373889,tt10128846","kind":"profile_collision"}, + {"nconst":"nm0409183","primaryName":"Neil Innes","birthYear":"1944","deathYear":"2019","primaryProfession":"actor,composer,music_department","knownForTitles":"tt0071853,tt0079470,tt0077147,tt0072558","kind":"profile_collision"}, + {"nconst":"nm0410968","primaryName":"Akira Ishihama","birthYear":"1935","deathYear":"2022","primaryProfession":"actor,soundtrack","knownForTitles":"tt0056058,tt0185812,tt0156669,tt0053114","kind":"profile_collision"}, + {"nconst":"nm0412917","primaryName":"Jaaved Jaaferi","birthYear":"1960","deathYear":"\\N","primaryProfession":"actor,music_department,producer","knownForTitles":"tt0456165,tt9531772,tt2905838,tt1105709","kind":"profile_collision"}, + {"nconst":"nm0413168","primaryName":"Hugh Jackman","birthYear":"1968","deathYear":"\\N","primaryProfession":"actor,producer,executive","knownForTitles":"tt1707386,tt3315342,tt0458525,tt0120903","kind":"profile_collision"}, + {"nconst":"nm0413271","primaryName":"Anne Jackson","birthYear":"1925","deathYear":"2016","primaryProfession":"actress,producer,archive_footage","knownForTitles":"tt0081505,tt0065642,tt0831400,tt0063102","kind":"profile_collision"}, + {"nconst":"nm0421772","primaryName":"Roy Jenson","birthYear":"1927","deathYear":"2007","primaryProfession":"actor,stunts,archive_footage","knownForTitles":"tt0071315,tt0070723,tt0076070,tt0077523","kind":"profile_collision"}, + {"nconst":"nm0424060","primaryName":"Scarlett Johansson","birthYear":"1984","deathYear":"\\N","primaryProfession":"actress,producer,director","knownForTitles":"tt0335266,tt1798709,tt0848228,tt3480822","kind":"profile_collision"}, + {"nconst":"nm0428278","primaryName":"Jack Jones","birthYear":"1938","deathYear":"2024","primaryProfession":"music_department,actor,soundtrack","knownForTitles":"tt0099685,tt0083530,tt0093105,tt0266489","kind":"profile_collision"}, + {"nconst":"nm0429069","primaryName":"Rashida Jones","birthYear":"1976","deathYear":"\\N","primaryProfession":"actress,producer,writer","knownForTitles":"tt1266020,tt1405365,tt1285016,tt1155056","kind":"profile_collision"}, + {"nconst":"nm0429114","primaryName":"Richard T. Jones","birthYear":"1972","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt0119081,tt0183649,tt0369339,tt0443274","kind":"profile_collision"}, + {"nconst":"nm0434759","primaryName":"Wolf Kahler","birthYear":"1940","deathYear":"\\N","primaryProfession":"actor,miscellaneous,soundtrack","knownForTitles":"tt0082971,tt0077269,tt0451279,tt7560984","kind":"profile_collision"}, + {"nconst":"nm0449807","primaryName":"Steve Kerr","birthYear":"1965","deathYear":"\\N","primaryProfession":"actor,producer,archive_footage","knownForTitles":"tt2041488,tt4218012,tt21878148,tt37798645","kind":"profile_collision"}, + {"nconst":"nm0449984","primaryName":"Irvin Kershner","birthYear":"1923","deathYear":"2010","primaryProfession":"director,cinematographer,actor","knownForTitles":"tt0080684,tt0110725,tt0086006,tt0100502","kind":"profile_collision"}, + {"nconst":"nm0451383","primaryName":"Mukesh Khanna","birthYear":"1958","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt0158417,tt0389680,tt0242519,tt3595298","kind":"profile_collision"}, + {"nconst":"nm0452576","primaryName":"Ford Kiernan","birthYear":"1962","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt0281491,tt1924394,tt1830332,tt0224464","kind":"profile_collision"}, + {"nconst":"nm0455057","primaryName":"Matt King","birthYear":"1968","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt1172570,tt1032755,tt0387764,tt0374853","kind":"profile_collision"}, + {"nconst":"nm0456158","primaryName":"Jack Kirby","birthYear":"1917","deathYear":"1994","primaryProfession":"writer,miscellaneous,animation_department","knownForTitles":"tt1825683,tt0371746,tt1641384,tt4154796","kind":"profile_collision"}, + {"nconst":"nm0458342","primaryName":"Terry Klassen","birthYear":"1957","deathYear":"\\N","primaryProfession":"actor,miscellaneous,casting_department","knownForTitles":"tt0214341,tt0138919,tt0211793,tt0165046","kind":"profile_collision"}, + {"nconst":"nm0462030","primaryName":"Masaki Kobayashi","birthYear":"1916","deathYear":"1996","primaryProfession":"director,writer,assistant_director","knownForTitles":"tt0056058,tt0061847,tt0053114,tt0055233","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0462895","primaryName":"David Koepp","birthYear":"1963","deathYear":"\\N","primaryProfession":"writer,miscellaneous,producer","knownForTitles":"tt0107290,tt0164181,tt30988739,tt0117060","kind":"profile_collision"}, + {"nconst":"nm0463282","primaryName":"Kurt Koehler","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,casting_department,director","knownForTitles":"tt15398776,tt0898266,tt26670955,tt1266020","kind":"profile_collision"}, + {"nconst":"nm0465777","primaryName":"Kim Kopf","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,producer,writer","knownForTitles":"tt0910970,tt3092878,tt0120552,tt0114959","kind":"profile_collision"}, + {"nconst":"nm0466349","primaryName":"Baltasar Kormákur","birthYear":"1966","deathYear":"\\N","primaryProfession":"producer,writer,director","knownForTitles":"tt0805576,tt1114712,tt1764275,tt0237993","kind":"profile_collision"}, + {"nconst":"nm0471392","primaryName":"Eric Kripke","birthYear":"1974","deathYear":"\\N","primaryProfession":"writer,producer,director","knownForTitles":"tt1190634,tt0460681,tt13159924,tt0357507","kind":"profile_collision"}, + {"nconst":"nm0472816","primaryName":"Jack Kruschen","birthYear":"1922","deathYear":"2002","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0053604,tt0046534,tt0052564,tt0054016","kind":"profile_collision"}, + {"nconst":"nm0473166","primaryName":"Joachim Król","birthYear":"1957","deathYear":"\\N","primaryProfession":"actor,archive_footage","knownForTitles":"tt0109255,tt0130827,tt0155722,tt0246430","kind":"profile_collision"}, + {"nconst":"nm0484104","primaryName":"Sarah Lancashire","birthYear":"1964","deathYear":"\\N","primaryProfession":"actress,producer,director","knownForTitles":"tt3428912,tt8079248,tt0243183,tt0239958","kind":"profile_collision"}, + {"nconst":"nm0492639","primaryName":"Bill Lawrence","birthYear":"1968","deathYear":"\\N","primaryProfession":"producer,writer,director","knownForTitles":"tt10986410,tt0285403,tt15677150,tt1441109","kind":"profile_collision"}, + {"nconst":"nm0493257","primaryName":"Josh Lawson","birthYear":"1981","deathYear":"\\N","primaryProfession":"actor,writer,director","knownForTitles":"tt2785032,tt0293429,tt17490712,tt6023474","kind":"profile_collision"}, + {"nconst":"nm0500894","primaryName":"Brad Leland","birthYear":"1954","deathYear":"\\N","primaryProfession":"actor,producer","knownForTitles":"tt1860357,tt0758745,tt0448157,tt0390022","kind":"profile_collision"}, + {"nconst":"nm0501435","primaryName":"Kasi Lemmons","birthYear":"1959","deathYear":"\\N","primaryProfession":"actress,director,writer","knownForTitles":"tt0102926,tt4648786,tt0103919,tt0119080","kind":"profile_collision"}, + {"nconst":"nm0504962","primaryName":"Ken Leung","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt10954652,tt0411008,tt0141842,tt7671070","kind":"profile_collision"}, + {"nconst":"nm0507659","primaryName":"Richard Lewis","birthYear":"1947","deathYear":"2024","primaryProfession":"actor,writer,producer","knownForTitles":"tt0107977,tt0113627,tt0264235,tt0105980","kind":"profile_collision"}, + {"nconst":"nm0511892","primaryName":"Thure Lindhardt","birthYear":"1974","deathYear":"\\N","primaryProfession":"actor,director,producer","knownForTitles":"tt0920458,tt0808151,tt0403360,tt2011953","kind":"profile_collision"}, + {"nconst":"nm0514904","primaryName":"Chia-Hui Liu","birthYear":"1951","deathYear":"\\N","primaryProfession":"actor,stunts,director","knownForTitles":"tt0378194,tt0078243,tt0266697,tt1258972","kind":"profile_collision"}, + {"nconst":"nm0519765","primaryName":"Theodor Loos","birthYear":"1883","deathYear":"1954","primaryProfession":"actor,director,archive_footage","knownForTitles":"tt0022100,tt0017136,tt0013469,tt0132145","kind":"profile_collision"}, + {"nconst":"nm0526019","primaryName":"Diego Luna","birthYear":"1979","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt3748528,tt0245574,tt2262227,tt0316356","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0532461","primaryName":"Justina Machado","birthYear":"1972","deathYear":"\\N","primaryProfession":"actress,producer,soundtrack","knownForTitles":"tt0329691,tt0309593,tt0248654,tt5339440","kind":"profile_collision"}, + {"nconst":"nm0534134","primaryName":"Tress MacNeille","birthYear":"1951","deathYear":"\\N","primaryProfession":"actress,music_department,miscellaneous","knownForTitles":"tt0105941,tt0149460,tt0096697,tt0462538","kind":"profile_collision"}, + {"nconst":"nm0535582","primaryName":"Dominic Mafham","birthYear":"1968","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt5690810,tt14454876,tt3776288,tt3829170","kind":"profile_collision"}, + {"nconst":"nm0536883","primaryName":"Sean Maher","birthYear":"1975","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt0379786,tt0303461,tt6315800,tt5091548","kind":"profile_collision"}, + {"nconst":"nm0538320","primaryName":"Alan Mak","birthYear":"1965","deathYear":"\\N","primaryProfession":"director,actor,writer","knownForTitles":"tt0338564,tt1349853,tt0369060,tt0374339","kind":"profile_collision"}, + {"nconst":"nm0540441","primaryName":"Jena Malone","birthYear":"1984","deathYear":"\\N","primaryProfession":"actress,producer,music_department","knownForTitles":"tt1974419,tt1951266,tt0118884,tt1951264","kind":"profile_collision"}, + {"nconst":"nm0546816","primaryName":"Miriam Margolyes","birthYear":"1941","deathYear":"\\N","primaryProfession":"actress,soundtrack,archive_footage","knownForTitles":"tt0106226,tt0117509,tt0295297,tt0112453","kind":"profile_collision"}, + {"nconst":"nm0549134","primaryName":"John Marley","birthYear":"1907","deathYear":"1984","primaryProfession":"actor,archive_footage","knownForTitles":"tt0066011,tt0068646,tt0062952,tt0059017","kind":"profile_collision"}, + {"nconst":"nm0550855","primaryName":"E.G. Marshall","birthYear":"1914","deathYear":"1998","primaryProfession":"actor,writer,producer","knownForTitles":"tt0050083,tt0066473,tt0097958,tt0081573","kind":"profile_collision"}, + {"nconst":"nm0551065","primaryName":"Mona Marshall","birthYear":"1947","deathYear":"\\N","primaryProfession":"actress,miscellaneous,music_department","knownForTitles":"tt0121955,tt0210418,tt1799631,tt0363395","kind":"profile_collision"}, + {"nconst":"nm0552333","primaryName":"George R.R. Martin","birthYear":"1948","deathYear":"\\N","primaryProfession":"writer,producer,miscellaneous","knownForTitles":"tt0944947,tt11198330,tt0092319,tt27497448","kind":"profile_collision"}, + {"nconst":"nm0557850","primaryName":"Danny Mastrogiorgio","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,miscellaneous","knownForTitles":"tt4124758,tt4270492,tt7007696,tt2699110","kind":"profile_collision"}, + {"nconst":"nm0561982","primaryName":"James May","birthYear":"1963","deathYear":"\\N","primaryProfession":"writer,producer,actor","knownForTitles":"tt1628033,tt0857190,tt1536735,tt0498720","kind":"profile_collision"}, + {"nconst":"nm0562679","primaryName":"Peter Mayhew","birthYear":"1944","deathYear":"2019","primaryProfession":"actor,producer,miscellaneous","knownForTitles":"tt2488496,tt0076759,tt0080684,tt0086190","kind":"profile_collision"}, + {"nconst":"nm0566332","primaryName":"Michael McConnohie","birthYear":"1951","deathYear":"\\N","primaryProfession":"actor,writer,miscellaneous","knownForTitles":"tt1174954,tt0280240,tt0100758,tt0100263","kind":"profile_collision"}, + {"nconst":"nm0566393","primaryName":"Scott McCord","birthYear":"1971","deathYear":"\\N","primaryProfession":"actor,miscellaneous,music_department","knownForTitles":"tt9813792,tt10837952,tt1173427,tt1114855","kind":"profile_collision"}, + {"nconst":"nm0568390","primaryName":"Rob Mac","birthYear":"1977","deathYear":"\\N","primaryProfession":"writer,producer,actor","knownForTitles":"tt0472954,tt8879940,tt0345551,tt0118972","kind":"profile_collision"}, + {"nconst":"nm0568772","primaryName":"Mike McFarland","birthYear":"1970","deathYear":"\\N","primaryProfession":"actor,sound_department,producer","knownForTitles":"tt3819668,tt2263944,tt0388629,tt2560140","kind":"profile_collision"}, + {"nconst":"nm0572965","primaryName":"Pauline McLynn","birthYear":"1962","deathYear":"\\N","primaryProfession":"actress,archive_footage","knownForTitles":"tt0111958,tt9639470,tt0104231,tt3371366","kind":"profile_collision"}, + {"nconst":"nm0573862","primaryName":"Ian McNeice","birthYear":"1950","deathYear":"\\N","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0112281,tt0408381,tt0436992,tt0142032","kind":"profile_collision"}, + {"nconst":"nm0578766","primaryName":"Marc Menchaca","birthYear":"1975","deathYear":"\\N","primaryProfession":"actor,miscellaneous,director","knownForTitles":"tt5071412,tt1796960,tt3130776,tt2578508","kind":"profile_collision"}, + {"nconst":"nm0580014","primaryName":"Tobias Menzies","birthYear":"1974","deathYear":"\\N","primaryProfession":"actor,writer,soundtrack","knownForTitles":"tt16311594,tt16912512,tt15771916,tt4786824","kind":"profile_collision"}, + {"nconst":"nm0581282","primaryName":"Gary Merrill","birthYear":"1915","deathYear":"1990","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0042192,tt0041996,tt0043132,tt0057926","kind":"profile_collision"}, + {"nconst":"nm0582418","primaryName":"Laurie Metcalf","birthYear":"1955","deathYear":"\\N","primaryProfession":"actress,soundtrack,archive_footage","knownForTitles":"tt4925292,tt0102138,tt0120082,tt0133240","kind":"profile_collision"}, + {"nconst":"nm0583675","primaryName":"Menno Meyjes","birthYear":"1954","deathYear":"\\N","primaryProfession":"writer,director,script_department","knownForTitles":"tt0088939,tt0097576,tt0133952,tt2352230","kind":"profile_collision"}, + {"nconst":"nm0586528","primaryName":"Shin'ichirô Miki","birthYear":"1968","deathYear":"\\N","primaryProfession":"actor,music_department,miscellaneous","knownForTitles":"tt0156887,tt0375568,tt0210234,tt0235679","kind":"profile_collision"}, + {"nconst":"nm0586568","primaryName":"Mads Mikkelsen","birthYear":"1965","deathYear":"\\N","primaryProfession":"actor,producer,miscellaneous","knownForTitles":"tt2243973,tt2106476,tt1211837,tt10288566","kind":"profile_collision"}, + {"nconst":"nm0588087","primaryName":"Christopher Miller","birthYear":"1975","deathYear":"\\N","primaryProfession":"producer,writer,actor","knownForTitles":"tt1490017,tt12042730,tt0305011,tt9362722","kind":"profile_collision"}, + {"nconst":"nm0593348","primaryName":"Gavin Mitchell","birthYear":"1964","deathYear":"\\N","primaryProfession":"actor,archive_footage","knownForTitles":"tt1672845,tt0238793,tt6679794,tt0115751","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0594488","primaryName":"Kôhei Miyauchi","birthYear":"1929","deathYear":"1995","primaryProfession":"actor,archive_sound","knownForTitles":"tt0087544,tt0091211,tt0142240,tt0088509","kind":"profile_collision"}, + {"nconst":"nm0594503","primaryName":"Hayao Miyazaki","birthYear":"1941","deathYear":"\\N","primaryProfession":"animation_department,writer,art_department","knownForTitles":"tt0245429,tt0096283,tt0347149,tt0119698","kind":"profile_collision"}, + {"nconst":"nm0595567","primaryName":"Donald Moffat","birthYear":"1930","deathYear":"2018","primaryProfession":"actor,archive_footage","knownForTitles":"tt0084787,tt0109444,tt0086197,tt0081353","kind":"profile_collision"}, + {"nconst":"nm0597223","primaryName":"Janel Moloney","birthYear":"1969","deathYear":"\\N","primaryProfession":"actress,soundtrack,archive_footage","knownForTitles":"tt0200276,tt8134470,tt12677870,tt2699128","kind":"profile_collision"}, + {"nconst":"nm0598748","primaryName":"Felicity Montagu","birthYear":"1960","deathYear":"\\N","primaryProfession":"actress,director,soundtrack","knownForTitles":"tt0243155,tt0455538,tt0395404,tt0469021","kind":"profile_collision"}, + {"nconst":"nm0601553","primaryName":"Mandy Moore","birthYear":"1984","deathYear":"\\N","primaryProfession":"actress,producer,writer","knownForTitles":"tt0281358,tt0398286,tt0332375,tt0360139","kind":"profile_collision"}, + {"nconst":"nm0604948","primaryName":"Peter Morgan","birthYear":"1963","deathYear":"\\N","primaryProfession":"writer,producer,actor","knownForTitles":"tt0870111,tt4786824,tt0455590,tt0436697","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0606487","primaryName":"Dean Norris","birthYear":"1963","deathYear":"\\N","primaryProfession":"actor,producer,soundtrack","knownForTitles":"tt0903747,tt0103064,tt0100802,tt3704050","kind":"profile_collision"}, + {"nconst":"nm0611148","primaryName":"Wanja Mues","birthYear":"1973","deathYear":"\\N","primaryProfession":"actor,director,assistant_director","knownForTitles":"tt0253474,tt0372183,tt0121037,tt0891414","kind":"profile_collision"}, + {"nconst":"nm0613997","primaryName":"Christopher Murney","birthYear":"1943","deathYear":"\\N","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0101410,tt0091499,tt0115333,tt0072251","kind":"profile_collision"}, + {"nconst":"nm0614165","primaryName":"Cillian Murphy","birthYear":"1976","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt0411195,tt0289043,tt15398776,tt0421239","kind":"profile_collision"}, + {"nconst":"nm0614682","primaryName":"Ryan Murphy","birthYear":"1965","deathYear":"\\N","primaryProfession":"producer,writer,music_department","knownForTitles":"tt1327801,tt10161886,tt1844624,tt7562112","kind":"profile_collision"}, + {"nconst":"nm0616729","primaryName":"Harry Myers","birthYear":"1882","deathYear":"1938","primaryProfession":"actor,director,writer","knownForTitles":"tt0021749,tt1204019,tt0012067,tt0017654","kind":"profile_collision"}, + {"nconst":"nm0619684","primaryName":"Ghanshyam Nayak","birthYear":"1944","deathYear":"2021","primaryProfession":"actor,music_department","knownForTitles":"tt1708446,tt0133024,tt0374271,tt0347332","kind":"profile_collision"}, + {"nconst":"nm0619923","primaryName":"Olivier Nakache","birthYear":"1973","deathYear":"\\N","primaryProfession":"writer,producer,director","knownForTitles":"tt1675434,tt8655470,tt5699154,tt11080216","kind":"profile_collision"}, + {"nconst":"nm0619938","primaryName":"Tatsuya Nakadai","birthYear":"1932","deathYear":"2025","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0056058,tt0080979,tt0089881,tt0055233","kind":"profile_collision"}, + {"nconst":"nm0622288","primaryName":"Jeff Nathanson","birthYear":"1965","deathYear":"\\N","primaryProfession":"writer,script_department,producer","knownForTitles":"tt0264464,tt0120179,tt1790809,tt0293564","kind":"profile_collision"}, + {"nconst":"nm0625167","primaryName":"Barry Nelson","birthYear":"1917","deathYear":"2007","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0081505,tt0065377,tt0035959,tt0034172","kind":"profile_collision"}, + {"nconst":"nm0632769","primaryName":"Tomohiro Nishimura","birthYear":"1961","deathYear":"\\N","primaryProfession":"actor,sound_department,writer","knownForTitles":"tt0096283,tt0110008,tt2343539,tt0228456","kind":"profile_collision"}, + {"nconst":"nm0636435","primaryName":"Akiyuki Nosaka","birthYear":"1930","deathYear":"2015","primaryProfession":"writer,actor,archive_footage","knownForTitles":"tt0095327,tt0219359,tt0060560,tt0139864","kind":"profile_collision"}, + {"nconst":"nm0637586","primaryName":"Masako Nozawa","birthYear":"1936","deathYear":"\\N","primaryProfession":"actress,miscellaneous,soundtrack","knownForTitles":"tt2263944,tt3819668,tt0142251,tt0142235","kind":"profile_collision"}, + {"nconst":"nm0640193","primaryName":"Andrew O'Connor","birthYear":"\\N","deathYear":"\\N","primaryProfession":"producer,writer,actor","knownForTitles":"tt0387764,tt0841027,tt1800246,tt2758296","kind":"profile_collision"}, + {"nconst":"nm0640307","primaryName":"Donald O'Connor","birthYear":"1925","deathYear":"2003","primaryProfession":"actor,producer,director","knownForTitles":"tt0045152,tt0045899,tt0045592,tt0041479","kind":"profile_collision"}, + {"nconst":"nm0643000","primaryName":"Simon Oakland","birthYear":"1915","deathYear":"1983","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0054215,tt0055614,tt0062765,tt0055992","kind":"profile_collision"}, + {"nconst":"nm0643805","primaryName":"Uwe Ochsenknecht","birthYear":"1956","deathYear":"\\N","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt1885102","kind":"profile_collision"}, + {"nconst":"nm0644905","primaryName":"Oh Tae-kyung","birthYear":"1982","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt0364569,tt0107171,tt1606384,tt3118758","kind":"profile_collision"}, + {"nconst":"nm0648780","primaryName":"Yoko Ono","birthYear":"1933","deathYear":"\\N","primaryProfession":"director,actress,composer","knownForTitles":"tt14858658,tt32246771,tt9735318,tt1758830","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0656929","primaryName":"Josh Pais","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt10166622,tt7286456,tt2249007,tt0100758","kind":"profile_collision"}, + {"nconst":"nm0657333","primaryName":"Chuck Palahniuk","birthYear":"1962","deathYear":"\\N","primaryProfession":"writer,actor,producer","knownForTitles":"tt0137523,tt1024715,tt4546372,tt1773686","kind":"profile_collision"}, + {"nconst":"nm0658244","primaryName":"Geoffrey Palmer","birthYear":"1927","deathYear":"2020","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0120347,tt0838232,tt0110428,tt0119280","kind":"profile_collision"}, + {"nconst":"nm0661791","primaryName":"Park Chan-wook","birthYear":"1963","deathYear":"\\N","primaryProfession":"producer,writer,director","knownForTitles":"tt4016934,tt0364569,tt0762073,tt12477480","kind":"profile_collision"}, + {"nconst":"nm0661825","primaryName":"Grace Park","birthYear":"1974","deathYear":"\\N","primaryProfession":"actress,archive_footage","knownForTitles":"tt1600194,tt0407362,tt0314979,tt0165929","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0668845","primaryName":"Maxine Peake","birthYear":"1974","deathYear":"\\N","primaryProfession":"actress,director,writer","knownForTitles":"tt2980516,tt31514146,tt3814808,tt0468515","kind":"profile_collision"}, + {"nconst":"nm0669254","primaryName":"Daniel Zacapa","birthYear":"1951","deathYear":"\\N","primaryProfession":"actor,producer","knownForTitles":"tt1836195,tt0236493,tt0114369,tt0108037","kind":"profile_collision"}, + {"nconst":"nm0672225","primaryName":"Bob Penny","birthYear":"1935","deathYear":"2022","primaryProfession":"actor","knownForTitles":"tt0095647,tt0109830,tt0104952,tt0256415","kind":"profile_collision"}, + {"nconst":"nm0675490","primaryName":"Nehemiah Persoff","birthYear":"1919","deathYear":"2022","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0090633,tt0053291,tt0096320,tt0051207","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0677758","primaryName":"Sandro Petraglia","birthYear":"1947","deathYear":"\\N","primaryProfession":"writer,director,assistant_director","knownForTitles":"tt0346336,tt0770829,tt0418110,tt0104663","kind":"profile_collision"}, + {"nconst":"nm0680846","primaryName":"Todd Phillips","birthYear":"1970","deathYear":"\\N","primaryProfession":"producer,director,writer","knownForTitles":"tt7286456,tt1231583,tt0302886,tt11315808","kind":"profile_collision"}, + {"nconst":"nm0682960","primaryName":"Andreas Pietschmann","birthYear":"1969","deathYear":"\\N","primaryProfession":"actor,miscellaneous,writer","knownForTitles":"tt5753856,tt9319668,tt3146360,tt15341198","kind":"profile_collision"}, + {"nconst":"nm0683379","primaryName":"Mitch Pileggi","birthYear":"1952","deathYear":"\\N","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0103772,tt0443701,tt0106179,tt0098320","kind":"profile_collision"}, + {"nconst":"nm0684500","primaryName":"Dominique Pinon","birthYear":"1955","deathYear":"\\N","primaryProfession":"actor,music_department,miscellaneous","knownForTitles":"tt0101700,tt0118583,tt0211915,tt0112682","kind":"profile_collision"}, + {"nconst":"nm0703522","primaryName":"Pearce Quigley","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,miscellaneous,soundtrack","knownForTitles":"tt1023114,tt1182350,tt1212419,tt0366777","kind":"profile_collision"}, + {"nconst":"nm0710020","primaryName":"Joe Ranft","birthYear":"1960","deathYear":"2005","primaryProfession":"writer,actor,miscellaneous","knownForTitles":"tt0092695,tt0317219,tt0114709,tt0120363","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0710829","primaryName":"Anthony Rapp","birthYear":"1971","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt0294870,tt0106677,tt0268978,tt0092513","kind":"profile_collision"}, + {"nconst":"nm0714698","primaryName":"Lance Reddick","birthYear":"1962","deathYear":"2023","primaryProfession":"actor,producer,soundtrack","knownForTitles":"tt2911666,tt4425200,tt3502248,tt2334879","kind":"profile_collision"}, + {"nconst":"nm0717709","primaryName":"Kelly Reilly","birthYear":"1977","deathYear":"\\N","primaryProfession":"actress,producer,archive_footage","knownForTitles":"tt0414387,tt1020530,tt1907668,tt0988045","kind":"profile_collision"}, + {"nconst":"nm0724245","primaryName":"Michael Richards","birthYear":"1949","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt0098904,tt0098546,tt0100419,tt0109068","kind":"profile_collision"}, + {"nconst":"nm0728132","primaryName":"Leon Rippy","birthYear":"1949","deathYear":"\\N","primaryProfession":"actor,archive_footage","knownForTitles":"tt0187393,tt0271367,tt0348914,tt0111282","kind":"profile_collision"}, + {"nconst":"nm0728795","primaryName":"Paul Ritter","birthYear":"1966","deathYear":"2021","primaryProfession":"actor,soundtrack","knownForTitles":"tt0830515,tt0367959,tt1844923,tt0375920","kind":"profile_collision"}, + {"nconst":"nm0728812","primaryName":"Thelma Ritter","birthYear":"1902","deathYear":"1969","primaryProfession":"actress,soundtrack,archive_footage","knownForTitles":"tt0042192,tt0047396,tt0043792,tt0046187","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0733153","primaryName":"Tony Robinson","birthYear":"1946","deathYear":"\\N","primaryProfession":"producer,actor,writer","knownForTitles":"tt0108961,tt0096642,tt0096548,tt0212579","kind":"profile_collision"}, + {"nconst":"nm0739683","primaryName":"Erwin Rommel","birthYear":"1891","deathYear":"1944","primaryProfession":"miscellaneous,archive_footage","knownForTitles":"tt2457532,tt0035803,tt0120933,tt4634466","kind":"profile_collision"}, + {"nconst":"nm0741103","primaryName":"Misty Rosas","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,miscellaneous,stunts","knownForTitles":"tt12162902,tt8111088,tt1951264,tt0338526","kind":"profile_collision"}, + {"nconst":"nm0744834","primaryName":"Eli Roth","birthYear":"\\N","deathYear":"\\N","primaryProfession":"director,writer,actor","knownForTitles":"tt36893729,tt1448754,tt0361748,tt0450278","kind":"profile_collision"}, + {"nconst":"nm0745310","primaryName":"Bryce Papenbrook","birthYear":"1986","deathYear":"\\N","primaryProfession":"actor,miscellaneous,sound_department","knownForTitles":"tt2560140,tt1799631,tt2580046,tt1034314","kind":"profile_collision"}, + {"nconst":"nm0745751","primaryName":"Jonathan Roumie","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,producer,location_management","knownForTitles":"tt32306991,tt5622316,tt30787627,tt10098448","kind":"profile_collision"}, + {"nconst":"nm0746583","primaryName":"Nicholas Rowe","birthYear":"1966","deathYear":"\\N","primaryProfession":"actor,archive_footage","knownForTitles":"tt14728854,tt15565872,tt11646832,tt5936448","kind":"profile_collision"}, + {"nconst":"nm0747420","primaryName":"Andre Royo","birthYear":"1968","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt1512235,tt1714206,tt1564368,tt1999995","kind":"profile_collision"}, + {"nconst":"nm0751648","primaryName":"Joe Russo","birthYear":"1971","deathYear":"\\N","primaryProfession":"producer,director,writer","knownForTitles":"tt4154796,tt6710474,tt4154756,tt1843866","kind":"profile_collision"}, + {"nconst":"nm0753480","primaryName":"Daisuke Ryû","birthYear":"1957","deathYear":"2021","primaryProfession":"actor,archive_footage","knownForTitles":"tt0080979,tt0089881,tt0230780,tt0164707","kind":"profile_collision"}, + {"nconst":"nm0755133","primaryName":"Andrew Sachs","birthYear":"1930","deathYear":"2016","primaryProfession":"actor,writer,soundtrack","knownForTitles":"tt0072500,tt0082517,tt0219454,tt0075696","kind":"profile_collision"}, + {"nconst":"nm0759757","primaryName":"Aldo Sambrell","birthYear":"1931","deathYear":"2010","primaryProfession":"actor,director,writer","knownForTitles":"tt0059578,tt0060196,tt0070893,tt0099558","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0761836","primaryName":"William Sanderson","birthYear":"1944","deathYear":"\\N","primaryProfession":"actor,archive_footage","knownForTitles":"tt0083658,tt0116830,tt0348914,tt0844441","kind":"profile_collision"}, + {"nconst":"nm0766120","primaryName":"Brooke Satchwell","birthYear":"1980","deathYear":"\\N","primaryProfession":"actress,producer,archive_footage","knownForTitles":"tt0088580,tt10648342,tt13589004,tt1132600","kind":"profile_collision"}, + {"nconst":"nm0782968","primaryName":"Hubert Selby Jr.","birthYear":"1928","deathYear":"2004","primaryProfession":"writer,actor","knownForTitles":"tt0180093,tt0289944,tt0120081,tt0089384","kind":"profile_collision"}, + {"nconst":"nm0785227","primaryName":"Andy Serkis","birthYear":"1964","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt3450958,tt1318514,tt2103281,tt0167261","kind":"profile_collision"}, + {"nconst":"nm0786641","primaryName":"Brent Sexton","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,producer","knownForTitles":"tt0316465,tt0408790,tt1082807,tt1175491","kind":"profile_collision"}, + {"nconst":"nm0791877","primaryName":"Suzanne Shepherd","birthYear":"1934","deathYear":"2023","primaryProfession":"actress,archive_footage","knownForTitles":"tt0119558,tt0098554,tt0099685,tt0095690","kind":"profile_collision"}, + {"nconst":"nm0793782","primaryName":"Shin Goo","birthYear":"1936","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt0140825,tt0294252,tt11498038,tt0464651","kind":"profile_collision"}, + {"nconst":"nm0794002","primaryName":"Yoshiko Shinohara","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress","knownForTitles":"tt0095327,tt0078772,tt0496467,tt0182601","kind":"profile_collision"}, + {"nconst":"nm0795975","primaryName":"Joe Shuster","birthYear":"1914","deathYear":"1992","primaryProfession":"writer,miscellaneous,art_department","knownForTitles":"tt0078346,tt0770828,tt5950044,tt0086393","kind":"profile_collision"}, + {"nconst":"nm0802107","primaryName":"Dara Singh Randhawa","birthYear":"1928","deathYear":"2012","primaryProfession":"actor,director,producer","knownForTitles":"tt0347304,tt0213300,tt0213108,tt0158417","kind":"profile_collision"}, + {"nconst":"nm0812220","primaryName":"André Sogliuzzo","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,sound_department,miscellaneous","knownForTitles":"tt1055369,tt0128853,tt0338348,tt0491117","kind":"profile_collision"}, + {"nconst":"nm0823563","primaryName":"Mike Starr","birthYear":"1950","deathYear":"\\N","primaryProfession":"actor,producer,soundtrack","knownForTitles":"tt0109707,tt0109686,tt0098554,tt0100150","kind":"profile_collision"}, + {"nconst":"nm0825010","primaryName":"Joseph Stefano","birthYear":"1922","deathYear":"2006","primaryProfession":"miscellaneous,writer,producer","knownForTitles":"tt0054215,tt0155975,tt0056777,tt0092455","kind":"profile_collision"}, + {"nconst":"nm0827307","primaryName":"Bob Stephenson","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt11834150,tt4925292,tt1637706,tt0137523","kind":"profile_collision"}, + {"nconst":"nm0829032","primaryName":"Ray Stevenson","birthYear":"1964","deathYear":"2023","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt1509767,tt0450314,tt1416801,tt0800369","kind":"profile_collision"}, + {"nconst":"nm0833185","primaryName":"Sonny Strait","birthYear":"1965","deathYear":"\\N","primaryProfession":"actor,miscellaneous,sound_department","knownForTitles":"tt3819668,tt2263944,tt1409055,tt0214341","kind":"profile_collision"}, + {"nconst":"nm0834989","primaryName":"Jeremy Strong","birthYear":"1978","deathYear":"\\N","primaryProfession":"actor,production_department,producer","knownForTitles":"tt8368368,tt1596363,tt1070874,tt1872194","kind":"profile_collision"}, + {"nconst":"nm0836071","primaryName":"Wes Studi","birthYear":"1947","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt0104691,tt0113277,tt0132347,tt0118956","kind":"profile_collision"}, + {"nconst":"nm0847223","primaryName":"Isao Takahata","birthYear":"1935","deathYear":"2018","primaryProfession":"director,assistant_director,writer","knownForTitles":"tt2576852,tt0095327,tt0102587,tt0110008","kind":"profile_collision"}, + {"nconst":"nm0848801","primaryName":"Yukari Tamura","birthYear":"1976","deathYear":"\\N","primaryProfession":"actress,music_department,composer","knownForTitles":"tt1910272,tt2553376,tt21423786,tt0824043","kind":"profile_collision"}, + {"nconst":"nm0850733","primaryName":"Genndy Tartakovsky","birthYear":"1970","deathYear":"\\N","primaryProfession":"producer,writer,director","knownForTitles":"tt0278238,tt10332508,tt0361243,tt5220122","kind":"profile_collision"}, + {"nconst":"nm0853299","primaryName":"Vaughn Taylor","birthYear":"1911","deathYear":"1983","primaryProfession":"actor,archive_footage","knownForTitles":"tt0054215,tt0074597,tt0051459,tt0065446","kind":"profile_collision"}, + {"nconst":"nm0855564","primaryName":"Lee Tergesen","birthYear":"1965","deathYear":"\\N","primaryProfession":"actor,producer,soundtrack","knownForTitles":"tt0118421,tt0995832,tt0105793,tt0340855","kind":"profile_collision"}, + {"nconst":"nm0856012","primaryName":"John Terry","birthYear":"1950","deathYear":"\\N","primaryProfession":"actor,archive_footage","knownForTitles":"tt0093058,tt0093428,tt0105046,tt0443706","kind":"profile_collision"}, + {"nconst":"nm0860724","primaryName":"Sophie Thompson","birthYear":"1962","deathYear":"\\N","primaryProfession":"actress,soundtrack,archive_footage","knownForTitles":"tt0280707,tt0116191,tt0109831,tt0879870","kind":"profile_collision"}, + {"nconst":"nm0861075","primaryName":"R.H. Thomson","birthYear":"1947","deathYear":"\\N","primaryProfession":"actor,stunts,soundtrack","knownForTitles":"tt1352824,tt0084117,tt0083201,tt0157704","kind":"profile_collision"}, + {"nconst":"nm0862916","primaryName":"Tommy Tiernan","birthYear":"1969","deathYear":"\\N","primaryProfession":"actor,writer,director","knownForTitles":"tt5198068,tt0199314,tt11997646,tt1804497","kind":"profile_collision"}, + {"nconst":"nm0862932","primaryName":"Jacob Tierney","birthYear":"1979","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt35495073,tt1295072,tt4647692,tt0339827","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0866058","primaryName":"J.R.R. Tolkien","birthYear":"1892","deathYear":"1973","primaryProfession":"writer,miscellaneous,soundtrack","knownForTitles":"tt0120737,tt0167260,tt0167261,tt0903624","kind":"profile_collision"}, + {"nconst":"nm0868153","primaryName":"Giuseppe Tornatore","birthYear":"1956","deathYear":"\\N","primaryProfession":"director,writer,actor","knownForTitles":"tt1924396,tt0095765,tt0120731,tt0213847","kind":"profile_collision"}, + {"nconst":"nm0868219","primaryName":"Guillermo del Toro","birthYear":"1964","deathYear":"\\N","primaryProfession":"writer,producer,director","knownForTitles":"tt0457430,tt0167190,tt1488589,tt5580390","kind":"profile_collision"}, + {"nconst":"nm0871287","primaryName":"Henry Travers","birthYear":"1874","deathYear":"1965","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0038650,tt0036342,tt0024184,tt0033373","kind":"profile_collision"}, + {"nconst":"nm0872370","primaryName":"Frederick Treves","birthYear":"1925","deathYear":"2012","primaryProfession":"actor,archive_footage","knownForTitles":"tt0080678,tt0424463,tt0092360,tt0078351","kind":"profile_collision"}, + {"nconst":"nm0872910","primaryName":"Jasmine Trinca","birthYear":"1981","deathYear":"\\N","primaryProfession":"actress,director,writer","knownForTitles":"tt2515034,tt0208990,tt0346336,tt6306400","kind":"profile_collision"}, + {"nconst":"nm0874339","primaryName":"Donald Trump","birthYear":"1946","deathYear":"\\N","primaryProfession":"producer,actor,writer","knownForTitles":"tt0364782,tt0104431,tt0313737,tt0120533","kind":"profile_collision"}, + {"nconst":"nm0875362","primaryName":"Yôko Tsukasa","birthYear":"1934","deathYear":"\\N","primaryProfession":"actress,soundtrack,archive_footage","knownForTitles":"tt0055630,tt0410164,tt0060508,tt0052579","kind":"profile_collision"}, + {"nconst":"nm0875477","primaryName":"Keiko Tsushima","birthYear":"1926","deathYear":"2012","primaryProfession":"actress,script_department,soundtrack","knownForTitles":"tt0047478,tt0348003,tt0073918,tt0164078","kind":"profile_collision"}, + {"nconst":"nm0877244","primaryName":"James Turley","birthYear":"1929","deathYear":"2016","primaryProfession":"actor,assistant_director,stunts","knownForTitles":"tt0096684,tt0052520,tt0091281,tt0067258","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0878349","primaryName":"Joe Tuttle","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt5290382,tt0048845,tt12677870,tt7491982","kind":"profile_collision"}, + {"nconst":"nm0882581","primaryName":"Kenji Utsumi","birthYear":"1937","deathYear":"2013","primaryProfession":"actor,miscellaneous,soundtrack","knownForTitles":"tt2263944,tt0142371,tt0365498,tt0462422","kind":"profile_collision"}, + {"nconst":"nm0893257","primaryName":"Milo Ventimiglia","birthYear":"1977","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt0813715,tt1034032,tt5555260,tt0964539","kind":"profile_collision"}, + {"nconst":"nm0897845","primaryName":"Soledad Villamil","birthYear":"1969","deathYear":"\\N","primaryProfession":"actress,soundtrack,archive_footage","knownForTitles":"tt1305806,tt0210843,tt0378453,tt0204110","kind":"profile_collision"}, + {"nconst":"nm0905154","primaryName":"Lana Wachowski","birthYear":"1965","deathYear":"\\N","primaryProfession":"writer,producer,director","knownForTitles":"tt0133093,tt0115736,tt1617661,tt1371111","kind":"profile_collision"}, + {"nconst":"nm0908824","primaryName":"Randall Wallace","birthYear":"1949","deathYear":"\\N","primaryProfession":"writer,producer,director","knownForTitles":"tt0277434,tt0213149,tt0112573,tt1028576","kind":"profile_collision"}, + {"nconst":"nm0915208","primaryName":"Naomi Watts","birthYear":"1968","deathYear":"\\N","primaryProfession":"actress,producer,soundtrack","knownForTitles":"tt0166924,tt0360717,tt1649419,tt2562232","kind":"profile_collision"}, + {"nconst":"nm0916073","primaryName":"Danny Webb","birthYear":"1958","deathYear":"\\N","primaryProfession":"actor,stunts,archive_footage","knownForTitles":"tt0103644,tt0102797,tt1893256,tt0985699","kind":"profile_collision"}, + {"nconst":"nm0922035","primaryName":"Dominic West","birthYear":"1969","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt0306414,tt0299658,tt0450314,tt0356618","kind":"profile_collision"}, + {"nconst":"nm0924154","primaryName":"Shea Whigham","birthYear":"1969","deathYear":"\\N","primaryProfession":"actor,producer,archive_footage","knownForTitles":"tt1675192,tt0477139,tt2024469,tt1800241","kind":"profile_collision"}, + {"nconst":"nm0925418","primaryName":"Ruth White","birthYear":"1914","deathYear":"1969","primaryProfession":"actress,soundtrack","knownForTitles":"tt0056592,tt0064665,tt0061747,tt0062794","kind":"profile_collision"}, + {"nconst":"nm0926235","primaryName":"James Whitmore","birthYear":"1921","deathYear":"2009","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0111161,tt0047573,tt0073053,tt0041163","kind":"profile_collision"}, + {"nconst":"nm0931404","primaryName":"Olivia Williams","birthYear":"1968","deathYear":"\\N","primaryProfession":"actress,soundtrack,archive_footage","knownForTitles":"tt1139328,tt1174732,tt0128445,tt0167404","kind":"profile_collision"}, + {"nconst":"nm0932112","primaryName":"Mykelti Williamson","birthYear":"1957","deathYear":"\\N","primaryProfession":"actor,director,writer","knownForTitles":"tt0109830,tt4094724,tt2671706,tt0118880","kind":"profile_collision"}, + {"nconst":"nm0932302","primaryName":"Dave Willis","birthYear":"1970","deathYear":"\\N","primaryProfession":"writer,actor,producer","knownForTitles":"tt0297494,tt0457146,tt0455326,tt1486217","kind":"profile_collision"}, + {"nconst":"nm0933988","primaryName":"Rainn Wilson","birthYear":"1966","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt0386676,tt2490326,tt1512235,tt0465624","kind":"profile_collision"}, + {"nconst":"nm0934014","primaryName":"Richard Wilson","birthYear":"1936","deathYear":"\\N","primaryProfession":"actor,director,miscellaneous","knownForTitles":"tt0098882,tt0120483,tt0087892,tt0377981","kind":"profile_collision"}, + {"nconst":"nm0934318","primaryName":"Anna Wilson-Jones","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,producer","knownForTitles":"tt5137338,tt2085059,tt1789074,tt8740790","kind":"profile_collision"}, + {"nconst":"nm0943978","primaryName":"Keenan Wynn","birthYear":"1916","deathYear":"1986","primaryProfession":"actor,stunts,soundtrack","knownForTitles":"tt0057012,tt0062138,tt0078087,tt0038420","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm0945322","primaryName":"Kappei Yamaguchi","birthYear":"1965","deathYear":"\\N","primaryProfession":"actor,music_department,miscellaneous","knownForTitles":"tt0388629,tt0877057,tt0290223,tt0366621","kind":"profile_collision"}, + {"nconst":"nm0950784","primaryName":"Masayuki Yui","birthYear":"1947","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt0089881,tt0080979,tt0100998,tt0243609","kind":"profile_collision"}, + {"nconst":"nm0951520","primaryName":"Jonathan Zaccaï","birthYear":"1970","deathYear":"\\N","primaryProfession":"actor,director,writer","knownForTitles":"tt1226334,tt0955308,tt4063800,tt11703710","kind":"profile_collision"}, + {"nconst":"nm0957909","primaryName":"Ayelet Zurer","birthYear":"1969","deathYear":"\\N","primaryProfession":"actress,producer,archive_footage","knownForTitles":"tt18923754,tt0808151,tt0770828,tt0408306","kind":"profile_collision"}, + {"nconst":"nm0971135","primaryName":"Craig Parkinson","birthYear":"1976","deathYear":"\\N","primaryProfession":"actor,archive_footage","knownForTitles":"tt1341167,tt1548850,tt0421082,tt0437954","kind":"profile_collision"}, + {"nconst":"nm0997448","primaryName":"Penelope Markopoulou","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress","knownForTitles":"tt6280810,tt14395312,tt28693459,tt0270176","kind":"profile_collision"}, + {"nconst":"nm0999118","primaryName":"Michael Sinterniklaas","birthYear":"1972","deathYear":"\\N","primaryProfession":"actor,miscellaneous,producer","knownForTitles":"tt5311514,tt0417373,tt0318913,tt1474276","kind":"profile_collision"}, + {"nconst":"nm1002609","primaryName":"Nicholas Braun","birthYear":"1988","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt7660850,tt1292566,tt0405325,tt1659337","kind":"profile_collision"}, + {"nconst":"nm10040186","primaryName":"Asante Blackk","birthYear":"2001","deathYear":"\\N","primaryProfession":"actor,producer","knownForTitles":"tt5555260,tt7137906,tt12313074,tt7645334","kind":"profile_collision"}, + {"nconst":"nm1005180","primaryName":"Matt Wolpert","birthYear":"\\N","deathYear":"\\N","primaryProfession":"writer,producer,miscellaneous","knownForTitles":"tt7772588,tt2802850,tt2788432","kind":"profile_collision"}, + {"nconst":"nm1010540","primaryName":"Terence Winter","birthYear":"1960","deathYear":"\\N","primaryProfession":"producer,writer,actor","knownForTitles":"tt0141842,tt0993846,tt0979432,tt0430308","kind":"profile_collision"}, + {"nconst":"nm10406290","primaryName":"Conor McKenna","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt35495073","kind":"profile_collision"}, + {"nconst":"nm1056790","primaryName":"Nicholas Aaron","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt0185906,tt32278481,tt0379557,tt11771270","kind":"profile_collision"}, + {"nconst":"nm1064352","primaryName":"Tracy Reed","birthYear":"1941","deathYear":"2012","primaryProfession":"actress,archive_footage","knownForTitles":"tt0057012,tt0058586,tt0061452,tt0063962","kind":"profile_collision"}, + {"nconst":"nm1069587","primaryName":"Darshan Kumaar","birthYear":"1986","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt3742284,tt9544034,tt11045422,tt10811166","kind":"profile_collision"}, + {"nconst":"nm10729513","primaryName":"Candice van Litsenborgh","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,writer,producer","knownForTitles":"tt11737520,tt33058344,tt39371050,tt27072067","kind":"profile_collision"}, + {"nconst":"nm1080019","primaryName":"Takayuki Sugô","birthYear":"1952","deathYear":"\\N","primaryProfession":"actor,miscellaneous,archive_sound","knownForTitles":"tt3295046,tt2388184,tt3513498,tt2496120","kind":"profile_collision"}, + {"nconst":"nm1080418","primaryName":"Doug Allan","birthYear":"1951","deathYear":"2026","primaryProfession":"cinematographer,camera_department,assistant_director","knownForTitles":"tt0296310,tt1806234,tt1762300,tt0300880","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm1086543","primaryName":"Amanda Seyfried","birthYear":"1985","deathYear":"\\N","primaryProfession":"actress,producer,soundtrack","knownForTitles":"tt1707386,tt0795421,tt0377092,tt0989757","kind":"profile_collision"}, + {"nconst":"nm10916931","primaryName":"Elle Chapman","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,art_department,art_director","knownForTitles":"tt33204276,tt7405458,tt1844624","kind":"profile_collision"}, + {"nconst":"nm11005835","primaryName":"Megan Stalter","birthYear":"1990","deathYear":"\\N","primaryProfession":"actress,writer,producer","knownForTitles":"tt11815682,tt21108842,tt10999738,tt21335356","kind":"profile_collision"}, + {"nconst":"nm1101677","primaryName":"Jun Fukuyama","birthYear":"1978","deathYear":"\\N","primaryProfession":"actor,music_department,art_department","knownForTitles":"tt0994314,tt3837246,tt3944082,tt3215154","kind":"profile_collision"}, + {"nconst":"nm1102891","primaryName":"Kristen Schaal","birthYear":"1978","deathYear":"\\N","primaryProfession":"actress,miscellaneous,writer","knownForTitles":"tt0427152,tt1985966,tt3398228,tt0477051","kind":"profile_collision"}, + {"nconst":"nm11062620","primaryName":"Sukumar Tudu","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt23849204,tt35855020,tt27163202,tt27996020","kind":"profile_collision"}, + {"nconst":"nm11070988","primaryName":"Liz Schack","birthYear":"\\N","deathYear":"\\N","primaryProfession":"assistant_director,miscellaneous,producer","knownForTitles":"tt37885038,tt12181902,tt8284230,tt27201401","kind":"profile_collision"}, + {"nconst":"nm11170246","primaryName":"Mahender Bisht","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,director","knownForTitles":"tt13510660,tt33559171,tt9680440,tt27235410","kind":"profile_collision"}, + {"nconst":"nm1126340","primaryName":"Ryûnosuke Kamiki","birthYear":"1993","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt23289160,tt5311514,tt1474276,tt0245429","kind":"profile_collision"}, + {"nconst":"nm1128050","primaryName":"Chris Addison","birthYear":"1971","deathYear":"\\N","primaryProfession":"director,actor,writer","knownForTitles":"tt0459159,tt1226774,tt0463827,tt1759761","kind":"profile_collision"}, + {"nconst":"nm11287356","primaryName":"Tre Mosley","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,writer,composer","knownForTitles":"tt10332508,tt22004662,tt10492672,tt8545540","kind":"profile_collision"}, + {"nconst":"nm1130251","primaryName":"Bráulio Mantovani","birthYear":"1963","deathYear":"\\N","primaryProfession":"writer,producer,script_department","knownForTitles":"tt1555149,tt0861739,tt0317248,tt0857355","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm11413442","primaryName":"Anthony Pavone","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt37287335,tt15408560,tt36583300,tt12597948","kind":"profile_collision"}, + {"nconst":"nm1142392","primaryName":"Rafael Pettersson","birthYear":"1976","deathYear":"\\N","primaryProfession":"actor,director,writer","knownForTitles":"tt1733785,tt0465974,tt0997023,tt2071609","kind":"profile_collision"}, + {"nconst":"nm1148550","primaryName":"Ava DuVernay","birthYear":"1972","deathYear":"\\N","primaryProfession":"writer,producer,miscellaneous","knownForTitles":"tt4419214,tt1020072,tt7137906,tt1211890","kind":"profile_collision"}, + {"nconst":"nm1165110","primaryName":"Chris Hemsworth","birthYear":"1983","deathYear":"\\N","primaryProfession":"actor,producer,soundtrack","knownForTitles":"tt0848228,tt3501632,tt1735898,tt10648342","kind":"profile_collision"}, + {"nconst":"nm1176985","primaryName":"Dave Bautista","birthYear":"1969","deathYear":"\\N","primaryProfession":"actor,producer,miscellaneous","knownForTitles":"tt2015381,tt1856101,tt3896198,tt1411250","kind":"profile_collision"}, + {"nconst":"nm1179578","primaryName":"Joel Johnstone","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt5788792,tt12887536,tt3530726,tt1870479","kind":"profile_collision"}, + {"nconst":"nm1179580","primaryName":"Seu Jorge","birthYear":"1970","deathYear":"\\N","primaryProfession":"music_artist,music_department,actor","knownForTitles":"tt7825208,tt0362270,tt1555149,tt0317248","kind":"profile_collision"}, + {"nconst":"nm12036313","primaryName":"Mabel Li","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,archive_footage","knownForTitles":"tt21132302,tt13515614,tt35836656,tt10970762","kind":"profile_collision"}, + {"nconst":"nm1208167","primaryName":"Diane Kruger","birthYear":"1976","deathYear":"\\N","primaryProfession":"actress,producer,miscellaneous","knownForTitles":"tt0361748,tt5723272,tt0368891,tt1401152","kind":"profile_collision"}, + {"nconst":"nm1209966","primaryName":"Oscar Isaac","birthYear":"1979","deathYear":"\\N","primaryProfession":"actor,producer,soundtrack","knownForTitles":"tt0470752,tt2042568,tt2937898,tt1312221","kind":"profile_collision"}, + {"nconst":"nm1214435","primaryName":"Charlie Cox","birthYear":"1982","deathYear":"\\N","primaryProfession":"actor,producer,archive_footage","knownForTitles":"tt3322312,tt2980516,tt10872600,tt0486655","kind":"profile_collision"}, + {"nconst":"nm12174956","primaryName":"Im Sung-jae","birthYear":"1987","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt20600022,tt11535228,tt24640580,tt38958590","kind":"profile_collision"}, + {"nconst":"nm1226341","primaryName":"Alberto Guerra","birthYear":"1982","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt15837600,tt8714904,tt6981430,tt33253070","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm1227481","primaryName":"Veronica Falcón","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,director,writer","knownForTitles":"tt5071412,tt2077823,tt0870154,tt32612507","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm1227825","primaryName":"Dorte Warnøe Høgh","birthYear":"\\N","deathYear":"\\N","primaryProfession":"writer,script_department,director","knownForTitles":"tt1333640,tt10834220,tt2025899,tt0328844","kind":"profile_collision"}, + {"nconst":"nm1229640","primaryName":"Hoon Lee","birthYear":"1973","deathYear":"\\N","primaryProfession":"actor,writer,archive_footage","knownForTitles":"tt5743796,tt2017109,tt3473640,tt3502248","kind":"profile_collision"}, + {"nconst":"nm1235366","primaryName":"Bret McKenzie","birthYear":"1976","deathYear":"\\N","primaryProfession":"actor,writer,composer","knownForTitles":"tt0863046,tt1204342,tt2281587,tt1985019","kind":"profile_collision"}, + {"nconst":"nm12794521","primaryName":"Mitchell Robertson","birthYear":"1998","deathYear":"\\N","primaryProfession":"actor,writer,director","knownForTitles":"tt32767294,tt30242198,tt13431512,tt13610344","kind":"profile_collision"}, + {"nconst":"nm1279638","primaryName":"Noah Hawley","birthYear":"1967","deathYear":"\\N","primaryProfession":"writer,producer,director","knownForTitles":"tt2802850,tt13623632,tt5114356,tt4756228","kind":"profile_collision"}, + {"nconst":"nm1310525","primaryName":"Lee Sun-kyun","birthYear":"1975","deathYear":"2023","primaryProfession":"actor,manager,soundtrack","knownForTitles":"tt6751668,tt3697626,tt1515205,tt2181460","kind":"profile_collision"}, + {"nconst":"nm1315809","primaryName":"Stephanie Sheh","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,miscellaneous,casting_director","knownForTitles":"tt5311514,tt0434665,tt3717532,tt0988824","kind":"profile_collision"}, + {"nconst":"nm1321655","primaryName":"Christopher Markus","birthYear":"1969","deathYear":"\\N","primaryProfession":"writer,producer,script_department","knownForTitles":"tt4154796,tt7766378,tt4154756,tt0458339","kind":"profile_collision"}, + {"nconst":"nm1321656","primaryName":"Stephen McFeely","birthYear":"1970","deathYear":"\\N","primaryProfession":"writer,producer,script_department","knownForTitles":"tt4154796,tt7766378,tt0458339,tt0363771","kind":"profile_collision"}, + {"nconst":"nm1334512","primaryName":"Ravi Khanvilkar","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,music_department,soundtrack","knownForTitles":"tt1934231,tt1395054,tt0986264,tt1091229","kind":"profile_collision"}, + {"nconst":"nm1334869","primaryName":"Logan Marshall-Green","birthYear":"1976","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt2400463,tt1314655,tt6499752,tt2250912","kind":"profile_collision"}, + {"nconst":"nm13443877","primaryName":"Nebi Tolga Yilmaz","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt21308718,tt28075973,tt13675832,tt9355244","kind":"profile_collision"}, + {"nconst":"nm13597133","primaryName":"Akshit Grover","birthYear":"\\N","deathYear":"\\N","primaryProfession":"producer,actor,assistant_director","knownForTitles":"tt7927936,tt8809646,tt22185582,tt15151786","kind":"profile_collision"}, + {"nconst":"nm13720608","primaryName":"Red Tennant","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt34611082,tt4928814,tt0096555","kind":"profile_collision"}, + {"nconst":"nm1392160","primaryName":"Claudio von Planta","birthYear":"1962","deathYear":"\\N","primaryProfession":"cinematographer,director,camera_department","knownForTitles":"tt10575718,tt2262173,tt3006246,tt7426120","kind":"profile_collision"}, + {"nconst":"nm14126734","primaryName":"Jeena Platon","birthYear":"2017","deathYear":"\\N","primaryProfession":"actress","knownForTitles":"tt8421350,tt23148340,tt5489746,tt36721210","kind":"profile_collision"}, + {"nconst":"nm1437925","primaryName":"Omi Vaidya","birthYear":"1982","deathYear":"\\N","primaryProfession":"actor,writer,editor","knownForTitles":"tt1187043,tt1090195,tt11154196,tt30489972","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm14380726","primaryName":"Fatih Aker","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt13675832","kind":"profile_collision"}, + {"nconst":"nm1486911","primaryName":"Shahab Hosseini","birthYear":"1974","deathYear":"\\N","primaryProfession":"actor,director,producer","knownForTitles":"tt1832382,tt1360860,tt5186714,tt22182170","kind":"profile_collision"}, + {"nconst":"nm1488529","primaryName":"Ahmet Mümtaz Taylan","birthYear":"1965","deathYear":"\\N","primaryProfession":"actor,composer,soundtrack","knownForTitles":"tt1831164,tt1827487,tt7748244,tt5927420","kind":"profile_collision"}, + {"nconst":"nm1506981","primaryName":"Bashir Salahuddin","birthYear":"1976","deathYear":"\\N","primaryProfession":"writer,actor,producer","knownForTitles":"tt2334871,tt8310486,tt9272514,tt1745960","kind":"profile_collision"}, + {"nconst":"nm1512156","primaryName":"Kate Higgins","birthYear":"1969","deathYear":"\\N","primaryProfession":"actress,miscellaneous,music_department","knownForTitles":"tt5848272,tt3124992,tt5834262,tt3717532","kind":"profile_collision"}, + {"nconst":"nm1515989","primaryName":"Mark Proksch","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt7908628,tt3032476,tt0386676,tt1780441","kind":"profile_collision"}, + {"nconst":"nm1519739","primaryName":"Nika King","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,producer,writer","knownForTitles":"tt8772296,tt4971144,tt4507442,tt1378167","kind":"profile_collision"}, + {"nconst":"nm1537825","primaryName":"Sam Heughan","birthYear":"1980","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt3006802,tt1083448,tt0401019,tt0390197","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm1543989","primaryName":"Aml Ameen","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt1790864,tt13659918,tt0435680,tt11804152","kind":"profile_collision"}, + {"nconst":"nm1555488","primaryName":"Jessy Hodges","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,producer,writer","knownForTitles":"tt5348176,tt35946742,tt9686380,tt8324422","kind":"profile_collision"}, + {"nconst":"nm1569276","primaryName":"Chadwick Boseman","birthYear":"1976","deathYear":"2020","primaryProfession":"actor,producer,writer","knownForTitles":"tt1825683,tt3498820,tt2473602,tt4154756","kind":"profile_collision"}, + {"nconst":"nm1580911","primaryName":"Brian Baumgartner","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt0386676,tt0369436,tt0762114,tt0840149","kind":"profile_collision"}, + {"nconst":"nm1631435","primaryName":"Aimee Teegarden","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,producer,soundtrack","knownForTitles":"tt0758745,tt5519574,tt0498381,tt2657262","kind":"profile_collision"}, + {"nconst":"nm1637637","primaryName":"David W. Collins","birthYear":"1975","deathYear":"\\N","primaryProfession":"sound_department,actor,miscellaneous","knownForTitles":"tt8111088,tt20600980,tt36594331,tt1024923","kind":"profile_collision"}, + {"nconst":"nm1648520","primaryName":"Katie Leung","birthYear":"1987","deathYear":"\\N","primaryProfession":"actress,archive_footage","knownForTitles":"tt0373889,tt0330373,tt1201607,tt0926084","kind":"profile_collision"}, + {"nconst":"nm1659819","primaryName":"Jamie McPherson","birthYear":"\\N","deathYear":"\\N","primaryProfession":"cinematographer,camera_department,producer","knownForTitles":"tt0318912,tt3068194,tt1806234,tt1762300","kind":"profile_collision"}, + {"nconst":"nm1663252","primaryName":"Chris Chalk","birthYear":"1977","deathYear":"\\N","primaryProfession":"actor,director,writer","knownForTitles":"tt19244304,tt2077823,tt26693313,tt7137906","kind":"profile_collision"}, + {"nconst":"nm1683003","primaryName":"Hiro Shimono","birthYear":"1980","deathYear":"\\N","primaryProfession":"actor,soundtrack,archive_sound","knownForTitles":"tt0364863,tt11032374,tt33175825,tt32820897","kind":"profile_collision"}, + {"nconst":"nm1683768","primaryName":"Beau Garrett","birthYear":"1982","deathYear":"\\N","primaryProfession":"actress,soundtrack,archive_footage","knownForTitles":"tt1104001,tt0486576,tt0454970,tt2101383","kind":"profile_collision"}, + {"nconst":"nm1697606","primaryName":"Yûichi Nakamura","birthYear":"1980","deathYear":"\\N","primaryProfession":"actor,miscellaneous,soundtrack","knownForTitles":"tt6264654,tt1951264,tt14331144,tt3295046","kind":"profile_collision"}, + {"nconst":"nm1700426","primaryName":"Martyn Colbeck","birthYear":"\\N","deathYear":"\\N","primaryProfession":"camera_department,cinematographer,director","knownForTitles":"tt0795176,tt1762300,tt14389492,tt2230619","kind":"profile_collision"}, + {"nconst":"nm1725469","primaryName":"Florian Zeller","birthYear":"1979","deathYear":"\\N","primaryProfession":"writer,producer,director","knownForTitles":"tt10272386,tt14458442","kind":"profile_collision"}, + {"nconst":"nm1727134","primaryName":"Hüseyin Avni Danyal","birthYear":"1962","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt7920978,tt1807950,tt31064918,tt2869736","kind":"profile_collision"}, + {"nconst":"nm1745019","primaryName":"Maggie Siff","birthYear":"1974","deathYear":"\\N","primaryProfession":"actress,producer,soundtrack","knownForTitles":"tt0465580,tt1151359,tt2304933,tt1124373","kind":"profile_collision"}, + {"nconst":"nm1753818","primaryName":"Nick Shakoour","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,producer,miscellaneous","knownForTitles":"tt5622316,tt3489236,tt1600194,tt8975856","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm1784515","primaryName":"Jacky Ido","birthYear":"1977","deathYear":"\\N","primaryProfession":"actor,miscellaneous,director","knownForTitles":"tt30494226,tt0361748,tt3027506,tt4396862","kind":"profile_collision"}, + {"nconst":"nm1800339","primaryName":"Nalan Kuruçim","birthYear":"1971","deathYear":"\\N","primaryProfession":"actress","knownForTitles":"tt1831164,tt12140804,tt9124428,tt13231544","kind":"profile_collision"}, + {"nconst":"nm1819973","primaryName":"Ross Duffer","birthYear":"1984","deathYear":"\\N","primaryProfession":"writer,producer,director","knownForTitles":"tt4574334,tt2131532,tt0443696,tt2618986","kind":"profile_collision"}, + {"nconst":"nm1825214","primaryName":"John Mulaney","birthYear":"1982","deathYear":"\\N","primaryProfession":"writer,actor,producer","knownForTitles":"tt4633694,tt3513500,tt4677934,tt8271714","kind":"profile_collision"}, + {"nconst":"nm18295187","primaryName":"Graham Hendrix","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt31137273","kind":"profile_collision"}, + {"nconst":"nm1837590","primaryName":"Aaron Staton","birthYear":"1980","deathYear":"\\N","primaryProfession":"actor,writer","knownForTitles":"tt1764429,tt0804503,tt0489237,tt0426931","kind":"profile_collision"}, + {"nconst":"nm1862473","primaryName":"Chris Niosi","birthYear":"1988","deathYear":"\\N","primaryProfession":"actor,animation_department,writer","knownForTitles":"tt2146268,tt7303948,tt2140395,tt7197684","kind":"profile_collision"}, + {"nconst":"nm1872617","primaryName":"Katie Krentz","birthYear":"\\N","deathYear":"\\N","primaryProfession":"miscellaneous,producer,production_department","knownForTitles":"tt3061046,tt3718778,tt6790714,tt6114826","kind":"profile_collision"}, + {"nconst":"nm1877169","primaryName":"Myf Warhurst","birthYear":"1974","deathYear":"\\N","primaryProfession":"actress,writer,producer","knownForTitles":"tt7214082,tt2152910,tt6392144,tt32328599","kind":"profile_collision"}, + {"nconst":"nm1892960","primaryName":"Cengiz Bozkurt","birthYear":"1964","deathYear":"\\N","primaryProfession":"actor,soundtrack,casting_director","knownForTitles":"tt1831164,tt22855966,tt7944260,tt13231544","kind":"profile_collision"}, + {"nconst":"nm1934428","primaryName":"Sarita Joshi","birthYear":"1941","deathYear":"\\N","primaryProfession":"actress","knownForTitles":"tt23849204,tt4662064,tt20872936,tt10098288","kind":"profile_collision"}, + {"nconst":"nm1960813","primaryName":"Tony Napoli","birthYear":"1979","deathYear":"\\N","primaryProfession":"actor,stunts,producer","knownForTitles":"tt5944124,tt7235466,tt8702242,tt6037102","kind":"profile_collision"}, + {"nconst":"nm2031358","primaryName":"Lamorne Morris","birthYear":"1983","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt1826940,tt2802850,tt2704998,tt1634106","kind":"profile_collision"}, + {"nconst":"nm2055254","primaryName":"Sabrina Jalees","birthYear":"\\N","deathYear":"\\N","primaryProfession":"writer,actress,producer","knownForTitles":"tt5460226,tt7658402,tt16300138,tt5083928","kind":"profile_collision"}, + {"nconst":"nm2086079","primaryName":"Hiromu Arakawa","birthYear":"1973","deathYear":"\\N","primaryProfession":"writer,animation_department,miscellaneous","knownForTitles":"tt0485323,tt1355642,tt5607028,tt0444640","kind":"profile_collision"}, + {"nconst":"nm2105585","primaryName":"Tobias Lindholm","birthYear":"1977","deathYear":"\\N","primaryProfession":"writer,director,producer","knownForTitles":"tt10288566,tt2106476,tt2216240,tt3830162","kind":"profile_collision"}, + {"nconst":"nm2106637","primaryName":"Aziz Ansari","birthYear":"1983","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt1266020,tt4635276,tt27543578,tt0848537","kind":"profile_collision"}, + {"nconst":"nm2146022","primaryName":"Kôki Uchiyama","birthYear":"1990","deathYear":"\\N","primaryProfession":"actor,miscellaneous,soundtrack","knownForTitles":"tt2122656,tt1929675,tt5626028,tt1214085","kind":"profile_collision"}, + {"nconst":"nm2175486","primaryName":"Ryan Barger","birthYear":"\\N","deathYear":"\\N","primaryProfession":"editor,actor,visual_effects","knownForTitles":"tt1524150,tt6957316,tt3530232,tt5111552","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm2194189","primaryName":"Courtney A. Kemp","birthYear":"1977","deathYear":"\\N","primaryProfession":"writer,producer,miscellaneous","knownForTitles":"tt3281796,tt1442462,tt36022742,tt10732104","kind":"profile_collision"}, + {"nconst":"nm2196900","primaryName":"Janie Haddad Tompkins","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,director,writer","knownForTitles":"tt11815682,tt13798316,tt1710308,tt5755238","kind":"profile_collision"}, + {"nconst":"nm2215791","primaryName":"Juhi Parmar","birthYear":"1980","deathYear":"\\N","primaryProfession":"actress,archive_footage","knownForTitles":"tt8595766,tt1868745,tt2016894,tt8437668","kind":"profile_collision"}, + {"nconst":"nm2244205","primaryName":"Léa Seydoux","birthYear":"1985","deathYear":"\\N","primaryProfession":"actress,miscellaneous,soundtrack","knownForTitles":"tt2278871,tt2379713,tt2382320,tt1605783","kind":"profile_collision"}, + {"nconst":"nm2299231","primaryName":"Kira Buckland","birthYear":"1987","deathYear":"\\N","primaryProfession":"actress,miscellaneous,casting_director","knownForTitles":"tt2359704,tt9335498,tt5238626,tt11585486","kind":"profile_collision"}, + {"nconst":"nm2301950","primaryName":"Dakota Blue Richards","birthYear":"1994","deathYear":"\\N","primaryProfession":"actress,writer","knownForTitles":"tt0385752,tt0840196,tt2701582","kind":"profile_collision"}, + {"nconst":"nm2341751","primaryName":"Russell Harvard","birthYear":"1981","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt0469494,tt10192406,tt1094666,tt2802850","kind":"profile_collision"}, + {"nconst":"nm2353862","primaryName":"Dev Patel","birthYear":"1990","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt1010048,tt9214772,tt3741834,tt9243804","kind":"profile_collision"}, + {"nconst":"nm2358709","primaryName":"Nathan Fielder","birthYear":"1983","deathYear":"\\N","primaryProfession":"writer,actor,producer","knownForTitles":"tt13623608,tt10802170,tt2297757,tt8679236","kind":"profile_collision"}, + {"nconst":"nm2436722","primaryName":"Amber Nash","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,producer","knownForTitles":"tt1486217,tt4158110,tt21194586,tt15358446","kind":"profile_collision"}, + {"nconst":"nm2466457","primaryName":"Ryan Ridley","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt2861424,tt26584495,tt11358390,tt2876854","kind":"profile_collision"}, + {"nconst":"nm2493744","primaryName":"Hemant Kher","birthYear":"1977","deathYear":"\\N","primaryProfession":"writer,actor,director","knownForTitles":"tt12392504,tt20872936,tt35665851,tt16449766","kind":"profile_collision"}, + {"nconst":"nm2592482","primaryName":"Scott Nicholson","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,stunts,miscellaneous","knownForTitles":"tt7660850,tt0112864,tt0993846,tt0498399","kind":"profile_collision"}, + {"nconst":"nm2601399","primaryName":"Charles Bagli","birthYear":"\\N","deathYear":"\\N","primaryProfession":"\\N","knownForTitles":"tt4299972,tt0992930,tt9726390,tt4781664","kind":"profile_collision"}, + {"nconst":"nm2618951","primaryName":"Benjamin Hollingsworth","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,producer,archive_footage","knownForTitles":"tt9077530,tt4452630,tt5719748,tt1285309","kind":"profile_collision"}, + {"nconst":"nm2705651","primaryName":"Baris Serma","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt1534360,tt0855729,tt0949686","kind":"profile_collision"}, + {"nconst":"nm2728757","primaryName":"Stephen Rider","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt1517260,tt1327773,tt3322312,tt1599348","kind":"profile_collision"}, + {"nconst":"nm2739851","primaryName":"Ross Marquand","birthYear":"1981","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt1520211,tt4154756,tt6741278,tt10168312","kind":"profile_collision"}, + {"nconst":"nm2747851","primaryName":"Sukhwinder Chahal","birthYear":"1969","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt8291224,tt2377938,tt2658126,tt9680440","kind":"profile_collision"}, + {"nconst":"nm2796745","primaryName":"Adam Devine","birthYear":"1983","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt1981677,tt1610527,tt2452244,tt2823054","kind":"profile_collision"}, + {"nconst":"nm2832695","primaryName":"Deborah Ann Woll","birthYear":"1985","deathYear":"\\N","primaryProfession":"actress,writer,producer","knownForTitles":"tt0844441,tt5886046,tt1886493,tt3322312","kind":"profile_collision"}, + {"nconst":"nm2837894","primaryName":"Robbie Daymond","birthYear":"1982","deathYear":"\\N","primaryProfession":"actor,miscellaneous,sound_department","knownForTitles":"tt4834232,tt33039855,tt26453303,tt12343534","kind":"profile_collision"}, + {"nconst":"nm2858333","primaryName":"Justin Doescher","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,stunts,producer","knownForTitles":"tt0499097,tt4921338,tt6911418,tt5852918","kind":"profile_collision"}, + {"nconst":"nm2871845","primaryName":"Jacob Stringer","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt0469494","kind":"profile_collision"}, + {"nconst":"nm2887354","primaryName":"Girija Oak","birthYear":"1987","deathYear":"\\N","primaryProfession":"actress","knownForTitles":"tt15354916,tt0986264,tt31702041,tt8047762","kind":"profile_collision"}, + {"nconst":"nm2915105","primaryName":"Billy Magnussen","birthYear":"1985","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt11655566,tt2704998,tt1596363,tt30923123","kind":"profile_collision"}, + {"nconst":"nm2925304","primaryName":"Christina Chong","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,director,writer","knownForTitles":"tt1634122,tt12327578,tt0436992,tt1361336","kind":"profile_collision"}, + {"nconst":"nm2930503","primaryName":"Jack Reynor","birthYear":"1992","deathYear":"\\N","primaryProfession":"actor,director,writer","knownForTitles":"tt8291284,tt2109248,tt8772262,tt3544112","kind":"profile_collision"}, + {"nconst":"nm2945137","primaryName":"Nick Mohammed","birthYear":"1980","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt3659388,tt2112096,tt4827986,tt1473832","kind":"profile_collision"}, + {"nconst":"nm2969015","primaryName":"Lena Hall","birthYear":"1980","deathYear":"\\N","primaryProfession":"actress,soundtrack,music_artist","knownForTitles":"tt30459041,tt6156584,tt6150942,tt1723816","kind":"profile_collision"}, + {"nconst":"nm2975962","primaryName":"Alberto Ammann","birthYear":"1978","deathYear":"\\N","primaryProfession":"actor,producer,miscellaneous","knownForTitles":"tt1242422,tt2707408,tt1715336,tt22964884","kind":"profile_collision"}, + {"nconst":"nm2976800","primaryName":"Isa Briones","birthYear":"1999","deathYear":"\\N","primaryProfession":"actress,soundtrack,archive_footage","knownForTitles":"tt31938062,tt8806524,tt17720272,tt2788432","kind":"profile_collision"}, + {"nconst":"nm3016166","primaryName":"Robert Emms","birthYear":"1986","deathYear":"\\N","primaryProfession":"actor,composer,producer","knownForTitles":"tt7366338,tt9253284,tt1568911,tt3428912","kind":"profile_collision"}, + {"nconst":"nm3038143","primaryName":"Emily Atack","birthYear":"1989","deathYear":"\\N","primaryProfession":"actress,writer,camera_department","knownForTitles":"tt3038708,tt4423984,tt1772422,tt2175669","kind":"profile_collision"}, + {"nconst":"nm3086712","primaryName":"Piotr Michael","birthYear":"1988","deathYear":"\\N","primaryProfession":"actor,miscellaneous,writer","knownForTitles":"tt26443597,tt32580624,tt37146909,tt20674124","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm3091498","primaryName":"Lara Pulver","birthYear":"1980","deathYear":"\\N","primaryProfession":"actress,soundtrack,archive_footage","knownForTitles":"tt31510819,tt1475582,tt21636214,tt7631146","kind":"profile_collision"}, + {"nconst":"nm3119556","primaryName":"Sunita Rajwar","birthYear":"1969","deathYear":"\\N","primaryProfession":"actress,writer,assistant_director","knownForTitles":"tt12004706,tt10530900,tt27510174,tt8108202","kind":"profile_collision"}, + {"nconst":"nm3147751","primaryName":"LaKeith Stanfield","birthYear":"1991","deathYear":"\\N","primaryProfession":"actor,producer,soundtrack","knownForTitles":"tt5688932,tt2370248,tt8946378,tt1020072","kind":"profile_collision"}, + {"nconst":"nm3198781","primaryName":"Ben Aldridge","birthYear":"1985","deathYear":"\\N","primaryProfession":"actor,producer,soundtrack","knownForTitles":"tt15679400,tt7775720,tt13610872,tt5687612","kind":"profile_collision"}, + {"nconst":"nm3220898","primaryName":"Saba Azad","birthYear":"1990","deathYear":"\\N","primaryProfession":"actress,music_department,soundtrack","knownForTitles":"tt13868972,tt6540996,tt2057441,tt3447364","kind":"profile_collision"}, + {"nconst":"nm3222518","primaryName":"Sachet Engineer","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt0986264","kind":"profile_collision"}, + {"nconst":"nm3345234","primaryName":"David Carlyle","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt9140342,tt7493974,tt1533010","kind":"profile_collision"}, + {"nconst":"nm3348178","primaryName":"Geeta Agrawal Sharma","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,producer","knownForTitles":"tt21626284,tt23849204,tt13818368,tt28037987","kind":"profile_collision"}, + {"nconst":"nm3361199","primaryName":"Carly Chaikin","birthYear":"1990","deathYear":"\\N","primaryProfession":"actress,writer,producer","knownForTitles":"tt4158110,tt1741256,tt1294226,tt8427140","kind":"profile_collision"}, + {"nconst":"nm3402242","primaryName":"Donald Sales","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,choreographer,miscellaneous","knownForTitles":"tt2463208,tt11337908,tt16288838,tt7608248","kind":"profile_collision"}, + {"nconst":"nm3556828","primaryName":"Meg Washington","birthYear":"\\N","deathYear":"\\N","primaryProfession":"writer,composer,actress","knownForTitles":"tt7678620","kind":"profile_collision"}, + {"nconst":"nm3629668","primaryName":"Dalila Bela","birthYear":"2001","deathYear":"\\N","primaryProfession":"actress,soundtrack,music_artist","knownForTitles":"tt5421602,tt3900614,tt1843230,tt2023453","kind":"profile_collision"}, + {"nconst":"nm3633421","primaryName":"Sameer Rajda","birthYear":"1963","deathYear":"\\N","primaryProfession":"actor,writer","knownForTitles":"tt2283748,tt6748128,tt0154081,tt1339313","kind":"profile_collision"}, + {"nconst":"nm3634704","primaryName":"Marco D'Amore","birthYear":"1981","deathYear":"\\N","primaryProfession":"actor,director,writer","knownForTitles":"tt2049116,tt9048786,tt17495990,tt4517700","kind":"profile_collision"}, + {"nconst":"nm3665668","primaryName":"Sarp Akkaya","birthYear":"1980","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt10431500,tt3698408,tt2543258,tt2118669","kind":"profile_collision"}, + {"nconst":"nm3687902","primaryName":"Antonia Thomas","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,producer,writer","knownForTitles":"tt6470478,tt4051832,tt1548850,tt2481198","kind":"profile_collision"}, + {"nconst":"nm3726790","primaryName":"Mahdi Cocci","birthYear":"1980","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt8851668,tt30827810,tt10857160,tt11743610","kind":"profile_collision"}, + {"nconst":"nm3785071","primaryName":"Esben Dalgaard","birthYear":"1976","deathYear":"\\N","primaryProfession":"actor,writer,podcaster","knownForTitles":"tt10834220,tt10375972,tt7290748,tt3638488","kind":"profile_collision"}, + {"nconst":"nm3822030","primaryName":"Cristiana Dell'Anna","birthYear":"1985","deathYear":"\\N","primaryProfession":"actress,writer,producer","knownForTitles":"tt14351082,tt13276352,tt9526392,tt2049116","kind":"profile_collision"}, + {"nconst":"nm3849670","primaryName":"Faye Marsay","birthYear":"1986","deathYear":"\\N","primaryProfession":"actress,archive_footage","knownForTitles":"tt3169706,tt0944947,tt14369780,tt4555426","kind":"profile_collision"}, + {"nconst":"nm4002765","primaryName":"Faisal Rashid","birthYear":"1991","deathYear":"\\N","primaryProfession":"actor,producer","knownForTitles":"tt12392504,tt1334470,tt15128068,tt1871312","kind":"profile_collision"}, + {"nconst":"nm4043618","primaryName":"Tom Holland","birthYear":"1996","deathYear":"\\N","primaryProfession":"actor,producer,miscellaneous","knownForTitles":"tt2250912,tt6320628,tt3498820,tt1649419","kind":"profile_collision"}, + {"nconst":"nm4053358","primaryName":"Glenda MacInnis","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,producer","knownForTitles":"tt33041431,tt35826715,tt3283594,tt11055882","kind":"profile_collision"}, + {"nconst":"nm4095573","primaryName":"Emma Brennand","birthYear":"\\N","deathYear":"\\N","primaryProfession":"miscellaneous,producer,director","knownForTitles":"tt28445317,tt17405750,tt18750552,tt8779788","kind":"profile_collision"}, + {"nconst":"nm4129969","primaryName":"Andrija Kuzmanovic","birthYear":"1984","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt29318030,tt8737152,tt1634013,tt2160915","kind":"profile_collision"}, + {"nconst":"nm4232585","primaryName":"Griffin Burns","birthYear":"1987","deathYear":"\\N","primaryProfession":"actor,sound_department,miscellaneous","knownForTitles":"tt13292830,tt33130884,tt32820897,tt11585486","kind":"profile_collision"}, + {"nconst":"nm4245240","primaryName":"Fatih Artman","birthYear":"1988","deathYear":"\\N","primaryProfession":"actor,archive_footage","knownForTitles":"tt7439220,tt1795096,tt3283714,tt7642818","kind":"profile_collision"}, + {"nconst":"nm4260661","primaryName":"Sarah Dugdale","birthYear":"1995","deathYear":"\\N","primaryProfession":"actress","knownForTitles":"tt9077530,tt8110640,tt9165642,tt4864624","kind":"profile_collision"}, + {"nconst":"nm4263213","primaryName":"John Bradley","birthYear":"1988","deathYear":"\\N","primaryProfession":"actor,soundtrack,archive_footage","knownForTitles":"tt0944947,tt5834426,tt10223460,tt3458254","kind":"profile_collision"}, + {"nconst":"nm4297610","primaryName":"Kimia Hosseini","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress","knownForTitles":"tt1832382,tt5460508,tt16911980,tt3605138","kind":"profile_collision"}, + {"nconst":"nm4425051","primaryName":"Jack Quaid","birthYear":"1992","deathYear":"\\N","primaryProfession":"actor,writer,producer","knownForTitles":"tt1392170,tt29603959,tt26584495,tt11245972","kind":"profile_collision"}, + {"nconst":"nm4439231","primaryName":"Asami Seto","birthYear":"1993","deathYear":"\\N","primaryProfession":"actress,music_department,art_department","knownForTitles":"tt4272866,tt8993398,tt12343534,tt2176885","kind":"profile_collision"}, + {"nconst":"nm4439921","primaryName":"Von Lewis","birthYear":"1987","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt6966692,tt4465100,tt1490785,tt1754284","kind":"profile_collision"}, + {"nconst":"nm4482922","primaryName":"Sunny Hinduja","birthYear":"1990","deathYear":"\\N","primaryProfession":"actor,director,writer","knownForTitles":"tt14392248,tt41069741,tt9544034,tt28227737","kind":"profile_collision"}, + {"nconst":"nm4523607","primaryName":"Zarrin Darnell-Martin","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,actress,writer","knownForTitles":"tt10813940,tt10970762,tt1895587,tt10642244","kind":"profile_collision"}, + {"nconst":"nm4531936","primaryName":"Masha Cima","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,producer","knownForTitles":"tt7718110,tt3501074,tt1723816,tt5592230","kind":"profile_collision"}, + {"nconst":"nm4594069","primaryName":"Gideon Adlon","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,producer","knownForTitles":"tt2531344,tt5952594","kind":"profile_collision"}, + {"nconst":"nm4597034","primaryName":"Eray Eserol","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,art_director,casting_director","knownForTitles":"tt1795096,tt3283714,tt21095396,tt2010914","kind":"profile_collision"}, + {"nconst":"nm4641403","primaryName":"Thomas McNamara","birthYear":"1993","deathYear":"\\N","primaryProfession":"actor,miscellaneous,writer","knownForTitles":"tt10802170,tt15739916,tt0364845,tt13623136","kind":"profile_collision"}, + {"nconst":"nm4704928","primaryName":"Marc Rissmann","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,miscellaneous,director","knownForTitles":"tt0944947,tt1740299,tt13991232,tt0899043","kind":"profile_collision"}, + {"nconst":"nm5049751","primaryName":"Michaela Jaé (MJ) Rodriguez","birthYear":"1991","deathYear":"\\N","primaryProfession":"actress,miscellaneous,soundtrack","knownForTitles":"tt5090568,tt7562112,tt14271498,tt8721424","kind":"profile_collision"}, + {"nconst":"nm5105119","primaryName":"Ross Anderson","birthYear":"1987","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt8364368,tt1809398,tt2884018,tt2923780","kind":"profile_collision"}, + {"nconst":"nm5157662","primaryName":"Lulu Wilson","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,producer,animation_department","knownForTitles":"tt6763664,tt37209937,tt20916568,tt5140878","kind":"profile_collision"}, + {"nconst":"nm5211886","primaryName":"Jill Harris","birthYear":"1994","deathYear":"\\N","primaryProfession":"actress,miscellaneous,assistant_director","knownForTitles":"tt5862798,tt7441658,tt5839732,tt15433166","kind":"profile_collision"}, + {"nconst":"nm5224688","primaryName":"Laura Nirider","birthYear":"\\N","deathYear":"\\N","primaryProfession":"archive_footage","knownForTitles":"tt2130321,tt5189670,tt0103396","kind":"profile_collision"}, + {"nconst":"nm5318271","primaryName":"Cynthy Wu","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress","knownForTitles":"tt7772588,tt14403178,tt6877772,tt1691916","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm5358492","primaryName":"Kemp Powers","birthYear":"\\N","deathYear":"\\N","primaryProfession":"writer,director,producer","knownForTitles":"tt10612922,tt2948372,tt9362722,tt5700106","kind":"profile_collision"}, + {"nconst":"nm5511912","primaryName":"Salimata Kamate","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress","knownForTitles":"tt1675434,tt2800240,tt23546622,tt6556670","kind":"profile_collision"}, + {"nconst":"nm5551047","primaryName":"Kim Won-seok","birthYear":"\\N","deathYear":"\\N","primaryProfession":"director,writer","knownForTitles":"tt2745682,tt26471411,tt4240730,tt5332206","kind":"profile_collision"}, + {"nconst":"nm5584344","primaryName":"Yahya Abdul-Mateen II","birthYear":"1986","deathYear":"\\N","primaryProfession":"actor,producer,archive_footage","knownForTitles":"tt1477834,tt4592410","kind":"profile_collision"}, + {"nconst":"nm6016511","primaryName":"Finn Wolfhard","birthYear":"2002","deathYear":"\\N","primaryProfession":"actor,director,writer","knownForTitles":"tt1396484,tt4513678,tt4574334,tt1488589","kind":"profile_collision"}, + {"nconst":"nm6196619","primaryName":"Callan Potter","birthYear":"1998","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt5582392,tt35495073,tt6049106,tt21431644","kind":"profile_collision"}, + {"nconst":"nm6366417","primaryName":"Jefferson White","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,producer,director","knownForTitles":"tt4236770,tt17279496,tt11818818,tt5198890","kind":"profile_collision"}, + {"nconst":"nm6381704","primaryName":"RJ Walker","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,camera_department,miscellaneous","knownForTitles":"tt4288182,tt9064792,tt37094850,tt3468798","kind":"profile_collision"}, + {"nconst":"nm6449658","primaryName":"Shivankit Singh Parihar","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,writer,assistant_director","knownForTitles":"tt22185582,tt7665182,tt14392248,tt9422974","kind":"profile_collision"}, + {"nconst":"nm6564737","primaryName":"Joe Keery","birthYear":"1992","deathYear":"\\N","primaryProfession":"actor,composer,soundtrack","knownForTitles":"tt4574334,tt6264654,tt8879928,tt11394332","kind":"profile_collision"}, + {"nconst":"nm6631562","primaryName":"Genneya Walton","birthYear":"1999","deathYear":"\\N","primaryProfession":"actress","knownForTitles":"tt18923754,tt10062292,tt21328106,tt10311562","kind":"profile_collision"}, + {"nconst":"nm6771561","primaryName":"Lee Dong-hwi","birthYear":"1985","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt4016934,tt10530176,tt17329616,tt30485422","kind":"profile_collision"}, + {"nconst":"nm6875639","primaryName":"Sameer Saxena","birthYear":"1980","deathYear":"\\N","primaryProfession":"producer,writer,director","knownForTitles":"tt8595766,tt19072562,tt31183656,tt6005644","kind":"profile_collision"}, + {"nconst":"nm6955079","primaryName":"Steven Canals","birthYear":"1980","deathYear":"\\N","primaryProfession":"writer,producer,director","knownForTitles":"tt7562112,tt14903834,tt5210998,tt10166602","kind":"profile_collision"}, + {"nconst":"nm7051533","primaryName":"Alexa Demie","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,director","knownForTitles":"tt8772296,tt8652728,tt5613484","kind":"profile_collision"}, + {"nconst":"nm7054048","primaryName":"Lee Hyeri","birthYear":"1994","deathYear":"\\N","primaryProfession":"actress","knownForTitles":"tt35683421,tt5182866,tt29419864,tt22872644","kind":"profile_collision"}, + {"nconst":"nm7083517","primaryName":"Mallorie Rodak","birthYear":"1987","deathYear":"\\N","primaryProfession":"actress,miscellaneous","knownForTitles":"tt22248376,tt1528406,tt15399640,tt9054364","kind":"profile_collision"}, + {"nconst":"nm7159176","primaryName":"Debrah Lee Charatan","birthYear":"\\N","deathYear":"\\N","primaryProfession":"archive_footage","knownForTitles":"\\N","kind":"profile_collision"}, + {"nconst":"nm7583837","primaryName":"Abby Trott","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,sound_department,miscellaneous","knownForTitles":"tt7042146,tt9335498,tt5238626,tt5370130","kind":"profile_collision"}, + {"nconst":"nm7609875","primaryName":"Naomi Ackie","birthYear":"1991","deathYear":"\\N","primaryProfession":"actress,producer,soundtrack","knownForTitles":"tt14858658,tt12299608,tt4291600,tt32843349","kind":"profile_collision"}, + {"nconst":"nm7650310","primaryName":"John Griffin","birthYear":"\\N","deathYear":"\\N","primaryProfession":"writer,producer,actor","knownForTitles":"tt5264838,tt9813792,tt2583620","kind":"profile_collision"}, + {"nconst":"nm7831367","primaryName":"Cooper Tomlinson","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,producer,writer","knownForTitles":"tt37287335,tt38120758,tt34991493,tt22075376","kind":"profile_collision"}, + {"nconst":"nm7919715","primaryName":"Faithe Herman","birthYear":"2007","deathYear":"\\N","primaryProfession":"actress,archive_footage","knownForTitles":"tt0448115,tt10151854,tt5555260,tt11372418","kind":"profile_collision"}, + {"nconst":"nm7946031","primaryName":"Kıvanç Kılınç","birthYear":"1982","deathYear":"\\N","primaryProfession":"actor,writer","knownForTitles":"tt13675832,tt6345486,tt0791612,tt2356860","kind":"profile_collision"}, + {"nconst":"nm7947860","primaryName":"Kim Jung-hyun","birthYear":"1990","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt7020608,tt5209264,tt10850932,tt25699926","kind":"profile_collision"}, + {"nconst":"nm8037342","primaryName":"Lukita Maxwell","birthYear":"2001","deathYear":"\\N","primaryProfession":"actress","knownForTitles":"tt15677150,tt26657236,tt11454722,tt18375360","kind":"profile_collision"}, + {"nconst":"nm8228340","primaryName":"Liv Symone","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress","knownForTitles":"tt11761176,tt11378946,tt8080122","kind":"profile_collision"}, + {"nconst":"nm8258018","primaryName":"Cha Eun-woo","birthYear":"1997","deathYear":"\\N","primaryProfession":"actor,writer,soundtrack","knownForTitles":"tt8585954,tt14166656,tt29006388,tt17494686","kind":"canonicalization_of_uncertain_alias_or_profile"}, + {"nconst":"nm8311745","primaryName":"Jasmeet Singh Bhatia","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor,writer,director","knownForTitles":"tt6328940,tt6108090,tt32379268,tt13387102","kind":"profile_collision"}, + {"nconst":"nm8341039","primaryName":"Paxton Singleton","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt6763664,tt0460681,tt5486670,tt6550978","kind":"profile_collision"}, + {"nconst":"nm8360867","primaryName":"Indya Moore","birthYear":"1995","deathYear":"\\N","primaryProfession":"actress,writer,producer","knownForTitles":"tt9844522,tt8722346,tt9663764,tt7562112","kind":"profile_collision"}, + {"nconst":"nm8388409","primaryName":"Sky Alexis","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,miscellaneous","knownForTitles":"tt28066777,tt1488589,tt4729430,tt10687464","kind":"profile_collision"}, + {"nconst":"nm8621588","primaryName":"Luis Rodríguez","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt0059578,tt0085487,tt0059016,tt0044491","kind":"profile_collision"}, + {"nconst":"nm8784315","primaryName":"Vijay Kumar Dogra","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt23849204,tt4934950,tt5764096,tt4169250","kind":"profile_collision"}, + {"nconst":"nm9458718","primaryName":"Moses Ingram","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actress,writer,miscellaneous","knownForTitles":"tt10095582,tt10048342,tt12392556,tt7697412","kind":"profile_collision"}, + {"nconst":"nm9862860","primaryName":"Boluwatife Treasure Bankole","birthYear":"\\N","deathYear":"\\N","primaryProfession":"actor","knownForTitles":"tt8267604","kind":"profile_collision"}, + {"nconst":"nm9925449","primaryName":"Revathi Pillai","birthYear":"2002","deathYear":"\\N","primaryProfession":"actress","knownForTitles":"tt22001978,tt8595766,tt11854694,tt9432978","kind":"profile_collision"} + ] +} diff --git a/sites/imdb/seed_data.py b/sites/imdb/seed_data.py new file mode 100644 index 00000000..0feb21b1 --- /dev/null +++ b/sites/imdb/seed_data.py @@ -0,0 +1,732 @@ +"""IMDb mirror seed data — loads from scraped_data/*.json. + +Phase 1 (clone-website) loader. Pulls real catalog from Playwright recon: + scraped_data/chart_top.json — Top 250 ordered list + scraped_data/chart_toptv.json + scraped_data/chart_moviemeter.json + scraped_data/chart_boxoffice.json + scraped_data/title_.json — per-title detail + scraped_data/name_.json — per-person detail + +Outputs into the SQLAlchemy DB: + - 18 canonical Genre rows (skill recommends 5-15 categories; IMDb has 18) + - All scraped Title rows (typed movie / tvSeries) + - All scraped Person rows linked via Credit + - 4 benchmark users (alice.j / bob.c / carol.d / david.k @ test.com, + password TestPass123! — skill convention) + - Per-user seeded watchlist (4) / ratings (4-6) / one review + - Curated featured reviews per major title + +Idempotent: returns immediately if titles already populated. +Image files are referenced as `static/images/.jpg` etc. — they live +under `static/images/` (HF-managed in the WebHarbor pipeline). + +Data integrity safeguards (added after diagnosing scraper output): + - html.unescape on all string fields (IMDb ld.name emits ' etc.) + - prefer hero h1 (stripped) over ld.name (ld.name is in original language) + - canonical-URL guard: skip JSON whose ld.url tt_id != filename tt_id + (IMDb sometimes redirects an unknown tt_id to a different page; without + this guard, BENCH_USERS' Spirited Away (tt0245429) silently became + Psycho because IMDb returned the wrong page) + - case-insensitive substring match on box-office detail keys +""" +import html +import json +import re +import shutil +from datetime import date, datetime +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent +SCRAPED = BASE_DIR / 'scraped_data' +STATIC_IMG = BASE_DIR / 'static' / 'images' +STATIC_IMG.mkdir(parents=True, exist_ok=True) + +# Pin reference date so any "today/latest" rendering is stable per skill. +MIRROR_REFERENCE_DATE = datetime(2026, 5, 26) + + +# IMDb's 18 standard genre buckets (matches their genre browse pages). +GENRES = [ + ('Action', 'action'), ('Adventure', 'adventure'), ('Animation', 'animation'), + ('Biography', 'biography'), ('Comedy', 'comedy'), ('Crime', 'crime'), + ('Documentary', 'documentary'), ('Drama', 'drama'), ('Family', 'family'), + ('Fantasy', 'fantasy'), ('History', 'history'), ('Horror', 'horror'), + ('Music', 'music'), ('Mystery', 'mystery'), ('Romance', 'romance'), + ('Sci-Fi', 'sci-fi'), ('Thriller', 'thriller'), ('War', 'war'), + ('Western', 'western'), +] +GENRE_ALIAS = { + 'sci-fi': 'Sci-Fi', 'science fiction': 'Sci-Fi', 'science-fiction': 'Sci-Fi', + 'film-noir': 'Mystery', 'film noir': 'Mystery', 'short': 'Drama', + 'reality-tv': 'Documentary', 'talk-show': 'Documentary', 'game-show': 'Family', + 'news': 'Documentary', 'sport': 'Documentary', +} + + +# ------------ helpers ------------------------------------------------------ + +VOTES_RE = re.compile(r'^([\d.]+)\s*([KMB]?)', re.I) + + +def _u(s): + """html.unescape + strip; tolerant of None.""" + if s is None: + return '' + return html.unescape(str(s)).strip() + + +def _parse_votes(s): + """'3.2M' / '780K' / '12,345' → int.""" + if not s: + return 0 + s = s.strip().replace(',', '') + m = VOTES_RE.match(s) + if not m: + return 0 + num = float(m.group(1)) + suf = m.group(2).upper() if m.group(2) else '' + mult = {'K': 1_000, 'M': 1_000_000, 'B': 1_000_000_000}.get(suf, 1) + return int(num * mult) + + +def _parse_money(s): + """'$28,341,469 | bo_cumulativeworldwidegross | ...' → 28341469.""" + if not s: + return None + m = re.search(r'\$([\d,]+)', s) + if not m: + return None + try: + return int(m.group(1).replace(',', '')) + except ValueError: + return None + + +def _find_money(details, *needles): + """Look up detail value by case-insensitive substring of key. + + IMDb's data-testid keys are lowercase (bo_grossdomestic, + bo_cumulativeworldwidegross, bo_openingweekenddomestic). Earlier camelCase + lookups missed every entry, producing the all-zero box-office bug. + """ + if not details: + return None + lc = {k.lower(): v for k, v in details.items()} + for n in needles: + n_lc = n.lower() + for k, v in lc.items(): + if n_lc in k: + return _parse_money(v) + return None + + +DURATION_RE = re.compile(r'PT(?:(\d+)H)?(?:(\d+)M)?') + + +def _parse_duration(s): + if not s: + return None + m = DURATION_RE.match(s) + if not m: + return None + h = int(m.group(1) or 0) + mins = int(m.group(2) or 0) + return h * 60 + mins or None + + +YEAR_PAREN_RE = re.compile(r'\((\d{4})(?:[-–](\d{4})?)?\)') +DISAMBIG_RE = re.compile(r'\([IVX]+\)') # IMDb same-name suffix: (I), (II), (III), ... + + +def _h1_year(h1): + """'Inception(2010)' → 2010 ; '12 Angry Men(1957)' → 1957""" + m = YEAR_PAREN_RE.search(h1 or '') + if m: + try: + return int(m.group(1)), (int(m.group(2)) if m.group(2) else None) + except ValueError: + pass + return None, None + + +def _strip_year(h1): + s = YEAR_PAREN_RE.sub('', h1 or '') + s = DISAMBIG_RE.sub('', s) + return _u(s) + + +TT_FROM_URL = re.compile(r'/title/(tt\d+)') +NM_FROM_URL = re.compile(r'/name/(nm\d+)(?:[/#?]|$)') + + +def _ld_tt_id(ld): + if not ld: + return None + u = ld.get('url') or '' + m = TT_FROM_URL.search(u) + return m.group(1) if m else None + + +def _ld_nm_id(ld): + """Return the exact Person identity, or None for missing/malformed URLs.""" + url = (ld or {}).get('url') + match = NM_FROM_URL.search(url) if isinstance(url, str) else None + return match.group(1) if match else None + + +MONTH_NUMBERS = {name: number for number, name in enumerate(( + 'January', 'February', 'March', 'April', 'May', 'June', 'July', + 'August', 'September', 'October', 'November', 'December'), 1)} +RELEASE_DATE_RE = re.compile( + r'^(?:Release date\s*\|\s*)?(' + + '|'.join(MONTH_NUMBERS) + + r')\s+(\d{1,2}),\s+(\d{4})(?=\s|[|(]|$)') + + +def _parse_release_date(value): + """Extract an existing complete date; never invent a missing day/year.""" + text = _u(value) + try: + if re.fullmatch(r'\d{4}-\d{2}-\d{2}', text): + return date.fromisoformat(text).isoformat() + match = RELEASE_DATE_RE.match(text) + if match: + month, day, year = match.groups() + return date(int(year), MONTH_NUMBERS[month], int(day)).isoformat() + except ValueError: + pass + return '' + + +def _release_date_from_scrape(ld, details): + published = _u((ld or {}).get('datePublished')) + if re.fullmatch(r'\d{4}-\d{2}-\d{2}', published): + parsed = _parse_release_date(published) + if parsed: + return parsed + details = details or {} + return (_parse_release_date(details.get('releasedate')) + or _parse_release_date(details.get('releaseDate'))) + + +def _normalize_genre(name, name_to_genre): + key = (name or '').strip() + if not key: + return None + if key in name_to_genre: + return name_to_genre[key] + lo = key.lower() + canon = GENRE_ALIAS.get(lo) + if canon and canon in name_to_genre: + return name_to_genre[canon] + return None + + +def _release_year_from_ld(ld): + dp = (ld or {}).get('datePublished') or '' + m = re.match(r'^(\d{4})', dp) + return int(m.group(1)) if m else None + + +def _country_from_ld(ld): + """countryOfOrigin shape: [{'@type':'Country','name':'United States'}]""" + coo = (ld or {}).get('countryOfOrigin') or (ld or {}).get('country') + if isinstance(coo, list) and coo: + return coo[0].get('name', '') if isinstance(coo[0], dict) else str(coo[0]) + if isinstance(coo, dict): + return coo.get('name', '') + return '' + + +def _type_from_ld(ld): + t = (ld or {}).get('@type', '') + if t == 'TVSeries': + return 'tvSeries' + return 'movie' + + +def _copy_image(src_name, dst_name): + """Copy scraped image to static/images/. Returns relative path or ''.""" + src = SCRAPED / 'images' / src_name + if not src.exists() or src.stat().st_size == 0: + return '' + dst = STATIC_IMG / dst_name + if not dst.exists() or dst.stat().st_size != src.stat().st_size: + shutil.copy2(src, dst) + return dst_name + + +# ------------ chart rank map ---------------------------------------------- + +def _load_charts(): + """Return (top_rank, popularity_rank, box_office_us, top_tv_rank) dicts.""" + top_rank, pop_rank, top_tv_rank = {}, {}, {} + bo_us = {} + for chart, fname in [ + ('top', 'chart_top.json'), + ('toptv', 'chart_toptv.json'), + ('moviemeter', 'chart_moviemeter.json'), + ('boxoffice', 'chart_boxoffice.json'), + ]: + f = SCRAPED / fname + if not f.exists(): + continue + rows = json.loads(f.read_text()) + for i, r in enumerate(rows, start=1): + tt = r.get('tt_id') + if not tt: + continue + if chart == 'top': + top_rank[tt] = i + elif chart == 'toptv': + top_tv_rank[tt] = i + elif chart == 'moviemeter': + pop_rank[tt] = i + elif chart == 'boxoffice': + # box office chart rows have rating_str format too, + # but no money there; money comes from per-title details. + pass + return top_rank, top_tv_rank, pop_rank, bo_us + + +# ------------ benchmark users (skill canonical) --------------------------- + +USERS_SPEC = [ + {'email': 'alice.j@test.com', 'name': 'Alice Johnson', + 'password': 'TestPass123!'}, + {'email': 'bob.c@test.com', 'name': 'Bob Chen', + 'password': 'TestPass123!'}, + {'email': 'carol.d@test.com', 'name': 'Carol Davis', + 'password': 'TestPass123!'}, + {'email': 'david.k@test.com', 'name': 'David Kim', + 'password': 'TestPass123!'}, +] + + +# Featured (seeded) reviews. Headlines avoid leaking title-level facts that +# tasks may ask about (no "Best Picture 1994", no "Box office champion"). +SEED_REVIEWS = [ + # high-helpful reviews on the most-trafficked titles + ('tt0111161', 'CinemaPilgrim', + 'A timeless meditation on hope', + 'Frank Darabont turns the Stephen King novella into a slow-burning hymn to friendship. The performances feel lived-in; the cinematography stays out of the way. Watch it twice — the second viewing is even better.', + 10, 2413), + ('tt0111161', 'CelluloidDad', + 'Why this one stays at the top', + 'No special effects. No twist endings. Just two prisoners and a quiet question about whether anyone can be remade. Survives on craft alone.', + 10, 1902), + ('tt0068646', 'NewWaveFan', + 'The blueprint for the modern crime epic', + 'Every modern crime drama lives in the shadow of this 1972 work. The slow gathering of menace, the cool jazz strings, the wedding day staged like a state funeral — it has not been bettered.', + 10, 3120), + ('tt0468569', 'GothamFan', + 'A villain performance for the record books', + 'Forget the cape. The interrogation room scenes are some of the finest acting put to film in the 2000s. The realist Gotham makes every other comic-book movie look frivolous.', + 10, 4002), + ('tt1375666', 'DreamArchitect', + 'A summer blockbuster that asks you to keep up', + 'Five-tiered nested heist, an emotional core that earns its sentiment, and an ending that has fueled bar arguments for fifteen years now.', + 9, 1551), + ('tt0816692', 'DustBowlPilot', + 'Cosmic in the best sense', + 'Wears its Kubrick on its sleeve and earns the comparison. The docking sequence is the most exhilarating five minutes of the decade.', + 10, 1802), + ('tt0110912', 'ReservoirDad', + 'Pulp, defined', + 'Three years before the Coen brothers found their groove, Tarantino weaponized non-linear narrative for the mainstream. The diner conversation is still a master class.', + 10, 2287), + ('tt0167260', 'HobbitForever', + 'A trilogy ends in glory', + 'Twelve Academy Awards for a reason. Every character earns a quiet send-off, and the final harbour scene is the kind of farewell most franchises never figure out.', + 10, 1980), + ('tt0903747', 'AlbuquerqueDealer', + 'Television\'s greatest pivot', + 'A show that began as a quirky drama about a chemistry teacher with cancer ended as a Greek tragedy of a man who became his own worst fear. Cranston\'s arc has no equal on TV.', + 10, 3500), + ('tt0944947', 'IronThroneWatcher', + 'Eight seasons of grandeur and one of fumble', + 'For seven seasons, the gold standard of TV fantasy. The rushed final season cost it the crown but the first half remains required viewing.', + 8, 1320), + ('tt15398776', 'AtomicFilmgoer', + 'Three hours of conversation, not a minute wasted', + 'Mostly dialogue, mostly close-up. Murphy is hauntingly internal; the Trinity sequence is craft beyond superlative. Worth the IMAX premium.', + 10, 1710), + ('tt6751668', 'GenreNomad', + 'A class study in three acts', + 'Bong pivots from satire to thriller to tragedy without missing a beat. The first non-English Best Picture winner — every frame earned.', + 10, 1408), +] + + +# Watchlists / ratings / reviews per benchmark user. +# Picked to ensure disambiguation tasks: alice's watchlist has 4 items, +# carol's ratings span 5+ titles, etc. +USER_STATE = { + 'alice.j@test.com': { + 'watchlist': ['tt0111161', 'tt0468569', 'tt6751668', 'tt0816692'], + 'ratings': [('tt0111161', 10), ('tt0068646', 10), ('tt0468569', 9), + ('tt1375666', 9)], + 'reviews': [ + ('tt0816692', 'Cosmic in the best sense', + 'A film that wears its Kubrick on its sleeve and earns the comparison. The docking sequence alone is worth the IMAX premium.', 10), + ], + }, + 'bob.c@test.com': { + 'watchlist': ['tt0903747', 'tt0944947', 'tt4574334', 'tt0386676'], + 'ratings': [('tt0903747', 10), ('tt0944947', 9), ('tt0386676', 9), + ('tt0167260', 10), ('tt0120737', 10)], + 'reviews': [ + ('tt0386676', 'Comfort TV for the ages', + 'Twenty seasons in and the cold opens still make me laugh out loud.', 9), + ], + }, + 'carol.d@test.com': { + 'watchlist': ['tt0137523', 'tt0099685', 'tt0114369', 'tt0102926'], + 'ratings': [('tt0137523', 9), ('tt0114369', 8), ('tt0102926', 9), + ('tt0110912', 10), ('tt0099685', 9)], + 'reviews': [ + ('tt0137523', 'A movie that grows up with you', + 'Eye-rolling at 16, in love at 22, and at 40 I see it as a tragicomic indictment of consumerism.', 9), + ], + }, + 'david.k@test.com': { + 'watchlist': ['tt0080684', 'tt0078788', 'tt0050083', 'tt0167260'], + 'ratings': [('tt0080684', 10), ('tt0078788', 9), ('tt0050083', 10)], + 'reviews': [], + }, +} + + +# News items (12). related_tt and related_nm both supported. +NEWS = [ + ('Oppenheimer dominates the 96th Academy Awards', + 'Christopher Nolan\'s historical epic took home seven Oscars including Best Picture, Best Director, and Best Actor.', + 'IMDb News', '2024-03-11', 'movie', 'tt15398776'), + ('Stranger Things Season 5 wraps production', + 'The Duffer Brothers confirm filming has finished ahead of the show\'s final season premiere later this year.', + 'IMDb News', '2025-01-08', 'tv', 'tt4574334'), + ('Greta Gerwig signs on to direct Narnia adaptations', + 'After Barbie\'s global success, Gerwig will helm at least two Narnia films for Netflix.', + 'Variety', '2024-09-22', 'movie', 'tt1517268'), + ('Cillian Murphy joins Steve adaptation', + 'Following his Best Actor win, Murphy will star in and produce a Netflix film based on Cormac McCarthy\'s work.', + 'Deadline', '2024-06-30', 'celebrity', 'nm0614165'), + ('Peter Jackson returns to Middle-earth with The Hunt for Gollum', + 'Warner Bros. confirms Andy Serkis will direct with Jackson producing, slated for 2026.', + 'Hollywood Reporter', '2024-05-09', 'movie', 'tt0167260'), + ('Tom Hanks reflects on 30 years of Forrest Gump', + 'In a candid interview, Hanks revisits the making of the iconic 1994 drama.', + 'IndieWire', '2024-07-06', 'celebrity', 'tt0109830'), + ('Bong Joon-ho returns with Mickey 17', + 'The Parasite director\'s sci-fi follow-up starring Robert Pattinson hits theaters in March.', + 'IMDb News', '2025-02-14', 'movie', 'tt6751668'), + ('Robert Downey Jr. cast in Avengers: Doomsday', + 'In a surprise twist, Marvel announces Downey will return as Doctor Doom in 2026.', + 'Marvel Wire', '2024-07-27', 'celebrity', 'nm0000375'), + ('Quentin Tarantino\'s final film delayed', + 'Production on The Movie Critic has been put on hold; Tarantino is reconsidering the project.', + 'Deadline', '2024-04-18', 'movie', 'tt0110912'), + ('Anthony Hopkins begins memoir promotion', + 'The two-time Academy Award winner discusses six decades on screen.', + 'IMDb News', '2024-11-04', 'celebrity', 'nm0000164'), + ('Breaking Bad turns 17', + 'Looking back at how a chemistry teacher became the most quoted antihero on TV.', + 'AV Club', '2025-01-20', 'tv', 'tt0903747'), + ('Planet Earth III tops critic best-of lists', + 'Sir David Attenborough\'s latest series scores a 100 Metascore.', + 'IMDb News', '2024-12-29', 'tv', 'tt5491994'), +] + + +# ----------- main entrypoint ------------------------------------------------- + +def seed_all(db, Title, Person, Genre, Credit, Review, UserRating, + WatchlistItem, User, NewsItem): + """Idempotent. Returns silently if already populated.""" + if db.session.query(Title).count() > 0: + return + + # 1) Genres ------------------------------------------------------------ + name_to_genre = {} + for name, slug in GENRES: + g = Genre(name=name, slug=slug) + db.session.add(g) + name_to_genre[name] = g + db.session.flush() + + # 2) Load chart rank maps --------------------------------------------- + top_rank, top_tv_rank, pop_rank, _ = _load_charts() + + # 3) Persons (load first so credits link cleanly) --------------------- + nm_to_person = {} + skipped_garbage = 0 + skipped_person_identity = 0 + for f in sorted(SCRAPED.glob('name_*.json')): + nm_id = f.stem.removeprefix('name_') + try: + d = json.loads(f.read_text()) + except Exception: + continue + h1 = d.get('h1') or '' + # GARBAGE FILTER: IMDb anti-bot rate-limit sometimes serves a 403 + # / Error page. Don't ship those into the DB as real people. + if h1.startswith(('403', 'Error', '404', '502', '503')) or not h1.strip(): + skipped_garbage += 1 + continue + ld = d.get('ld') or {} + # A filename is the requested identity, not proof of the page returned. + # Fail closed for absent/malformed canonical URLs as well as redirects. + if _ld_nm_id(ld) != nm_id: + skipped_person_identity += 1 + continue + # Prefer h1 (stripped of year + (I)/(II) disambig) over ld.name. + name = _strip_year(h1) or _u(ld.get('name')) or '' + if not name: + continue + # birth_year / death_year: prefer ld.birthDate/deathDate, then h1, then born_block. + by = dy = None + bd = (ld.get('birthDate') or '').strip() + dd = (ld.get('deathDate') or '').strip() + if bd[:4].isdigit(): + by = int(bd[:4]) + if dd[:4].isdigit(): + dy = int(dd[:4]) + if not by: + m = YEAR_PAREN_RE.search(h1) + if m: + try: by = int(m.group(1)) + except ValueError: pass + if m and m.group(2): + try: dy = int(m.group(2)) + except ValueError: pass + if not by: + # new scraper produces 'Born\nJuly 30, 1970' + born_block = d.get('born_block') or '' + m = re.search(r'(\d{4})', born_block) + if m: + by = int(m.group(1)) + if not dy: + died_block = d.get('died_block') or '' + m = re.search(r'(\d{4})', died_block) + if m: + dy = int(m.group(1)) + prof = ', '.join(d.get('profession') or []) or _u(ld.get('jobTitle')) + bio = _u(d.get('bio')) or _u(ld.get('description')) + photo_path = _copy_image(f"{nm_id}.jpg", f"{nm_id}.jpg") if (SCRAPED / 'images' / f'{nm_id}.jpg').exists() else '' + known_for = d.get('known_for') or [] + p = Person(nm_id=nm_id, name=name, birth_year=by, death_year=dy, + birth_place='', bio=(bio or '')[:4000], + primary_profession=prof[:200], + photo_path=photo_path, + known_for_json=json.dumps(known_for)) + db.session.add(p) + nm_to_person[nm_id] = p + print(f"[seed] persons loaded={len(nm_to_person)}, garbage_skipped={skipped_garbage}, " + f"identity_skipped={skipped_person_identity}", flush=True) + db.session.flush() + + # 4) Titles + credits -------------------------------------------------- + tt_to_title = {} + redirect_skipped = 0 + for f in sorted(SCRAPED.glob('title_*.json')): + tt_id = f.stem.removeprefix('title_') + try: + d = json.loads(f.read_text()) + except Exception: + continue + # CANONICAL-URL GUARD: IMDb sometimes serves a different title's page + # for an unknown tt_id (Spirited Away → Psycho was observed). Drop any + # scraped file whose ld.url disagrees with its filename. + ld = d.get('ld') or {} + canonical = _ld_tt_id(ld) + if canonical and canonical != tt_id: + redirect_skipped += 1 + continue + if tt_id in tt_to_title: + continue # dedupe + h1 = d.get('h1') or '' + # Prefer h1 (original Japanese ld.name → en h1: 'Parasite' wins). + title_text = _strip_year(h1) or _u(ld.get('name')) or tt_id + year, end_year = _h1_year(h1) + if year is None: + year = _release_year_from_ld(ld) + ttype = _type_from_ld(ld) if ld.get('@type') else 'movie' + runtime = _parse_duration(ld.get('duration', '')) + mpaa = _u(ld.get('contentRating')) + plot = _u(d.get('plot')) or _u(ld.get('description')) + plot_short = _u(d.get('plot_short')) or plot[:300] + rating = d.get('rating') + if rating is None: + agg = ld.get('aggregateRating') or {} + rating = agg.get('ratingValue') + try: + rating = float(rating) if rating is not None else 0.0 + except (TypeError, ValueError): + rating = 0.0 + votes = _parse_votes(d.get('votes')) + if not votes: + agg = ld.get('aggregateRating') or {} + votes = int(agg.get('ratingCount') or 0) + country = _country_from_ld(ld) + language = '' + if ld.get('inLanguage'): + language = ld['inLanguage'] if isinstance(ld['inLanguage'], str) else '' + # box office: lowercase substring match (IMDb data-testid is lowercase) + details = d.get('details') or {} + bo_us = _find_money(details, 'grossdomestic') + bo_ww = _find_money(details, 'cumulativeworldwidegross', 'worldwidegross') + bo_open = _find_money(details, 'openingweekenddomestic', 'openingweekend') + budget = _find_money(details, 'budget') + release_date = _release_date_from_scrape(ld, details) + poster_path = _copy_image(f'{tt_id}.jpg', f'{tt_id}.jpg') if (SCRAPED / 'images' / f'{tt_id}.jpg').exists() else '' + + tagline = _u(d.get('tagline')) + + t = Title( + tt_id=tt_id, title_type=ttype, primary_title=title_text, + year=year, end_year=end_year, runtime_min=runtime, mpaa_rating=mpaa, + plot_short=plot_short[:480], plot=plot, + rating_avg=rating, num_votes=votes, + popularity_rank=pop_rank.get(tt_id), + top_rank=(top_rank.get(tt_id) or top_tv_rank.get(tt_id)), + box_office_us=bo_us, box_office_world=bo_ww, + box_office_opening=bo_open, budget=budget, + release_date=release_date, + country=country[:80], language=language[:80], + poster_path=poster_path, + taglines_json=json.dumps([tagline] if tagline else []), + ) + # Genres from ld.genre (array of strings or single) + g_raw = ld.get('genre') or d.get('genres') or [] + if isinstance(g_raw, str): + g_raw = [g_raw] + seen = set() + for gname in g_raw: + g = _normalize_genre(gname, name_to_genre) + if g is not None and g.id not in seen: + t.genres.append(g) + seen.add(g.id) + db.session.add(t) + tt_to_title[tt_id] = (t, d) + print(f"[seed] redirect_skipped={redirect_skipped}", flush=True) + db.session.flush() + + # 5) Credits ----------------------------------------------------------- + for tt_id, (t, d) in tt_to_title.items(): + seen = set() + # directors / writers / producers from credits dict + for role_key, role in [('director', 'director'), + ('writer', 'writer'), + ('producer', 'producer'), + ('creator', 'writer')]: + for pers in (d.get('credits') or {}).get(role_key) or []: + nm = pers.get('nm_id') + if not nm or (nm, role) in seen: + continue + p = nm_to_person.get(nm) + if p is None: + continue + db.session.add(Credit(title_id=t.id, person_id=p.id, + role=role, character='', billing_order=None)) + seen.add((nm, role)) + # cast + for c in (d.get('cast') or [])[:15]: + nm = c.get('nm_id') + if not nm or (nm, 'actor') in seen: + continue + p = nm_to_person.get(nm) + if p is None: + continue + db.session.add(Credit(title_id=t.id, person_id=p.id, + role='actor', + character=(c.get('character') or '')[:160], + billing_order=c.get('billing'))) + seen.add((nm, 'actor')) + db.session.flush() + + # 5b) Backfill known_for: pick 4 highest-voted titles per person ------ + for nm, p in nm_to_person.items(): + # Skip if scraper already produced ≥3 known_for entries + try: + existing = json.loads(p.known_for_json or '[]') + except Exception: + existing = [] + if len(existing) >= 3: + continue + their = [(c.title.num_votes or 0, c.title.tt_id) for c in p.credits if c.title] + their.sort(reverse=True) + seen, picked = set(), [] + for _, tt in their: + if tt in seen: continue + seen.add(tt); picked.append(tt) + if len(picked) >= 4: break + p.known_for_json = json.dumps(picked) + db.session.flush() + + # 6) Benchmark users --------------------------------------------------- + email_to_user = {} + for spec in USERS_SPEC: + u = User(email=spec['email'], name=spec['name']) + u.set_password(spec['password']) + db.session.add(u) + email_to_user[spec['email']] = u + db.session.flush() + + # 7) Featured review authors ------------------------------------------ + author_users = {} + for _, author, *_ in SEED_REVIEWS: + if author not in author_users: + u = User(email=f'{author.lower()}@imdb-mirror.test', name=author) + u.set_password('seeded-anon-' + author) + db.session.add(u) + author_users[author] = u + db.session.flush() + + # 8) Seeded reviews --------------------------------------------------- + for tt, author, headline, body, rating, helpful in SEED_REVIEWS: + entry = tt_to_title.get(tt) + if not entry: + continue + t = entry[0] + u = author_users[author] + r = Review(title_id=t.id, user_id=u.id, rating=rating, + headline=headline, body=body, helpful_count=helpful, + is_seed=True, + created_at=datetime(2024, 6, 1)) + db.session.add(r) + + # 9) Per-user state --------------------------------------------------- + for email, st in USER_STATE.items(): + u = email_to_user.get(email) + if u is None: + continue + for tt in st['watchlist']: + entry = tt_to_title.get(tt) + if not entry: + continue + db.session.add(WatchlistItem(user_id=u.id, title_id=entry[0].id, + added_at=datetime(2024, 5, 1))) + for tt, score in st['ratings']: + entry = tt_to_title.get(tt) + if not entry: + continue + db.session.add(UserRating(user_id=u.id, title_id=entry[0].id, + rating=score, + created_at=datetime(2024, 5, 5))) + for tt, headline, body, rating in st['reviews']: + entry = tt_to_title.get(tt) + if not entry: + continue + db.session.add(Review(title_id=entry[0].id, user_id=u.id, + rating=rating, headline=headline, body=body, + helpful_count=42, is_seed=False, + created_at=datetime(2024, 5, 10))) + + # 10) News ------------------------------------------------------------ + for headline, summary, source, pub, cat, related in NEWS: + db.session.add(NewsItem(headline=headline, summary=summary, + source=source, published_at=pub, + category=cat, related_tt=related)) + + db.session.commit() diff --git a/sites/imdb/seed_feature.py b/sites/imdb/seed_feature.py new file mode 100644 index 00000000..e9eab475 --- /dev/null +++ b/sites/imdb/seed_feature.py @@ -0,0 +1,139 @@ +"""Import a sourced IMDb editorial-page snapshot into ``home_features``. + +This is an explicit build-time migration. It validates that every rendered +image is already bundled locally and changes only the selected home feature. +""" +import argparse +from copy import deepcopy +import hashlib +import json +from pathlib import Path, PurePosixPath +import re +import sqlite3 +from urllib.parse import urlsplit + + +def digest(data): + return hashlib.sha256(data).hexdigest() + + +def _official_imdb_url(value): + parsed = urlsplit(value) + return parsed.scheme == 'https' and parsed.hostname == 'www.imdb.com' + + +def _validate_media(static, relative_path, expected_sha256): + path = PurePosixPath(relative_path) + if path.is_absolute() or '..' in path.parts or path.parts[:2] != ('images', 'home'): + raise ValueError('Feature media must use a local images/home path') + absolute = Path(static).joinpath(*path.parts) + if not absolute.is_file(): + raise ValueError('Feature media is missing: ' + relative_path) + actual = digest(absolute.read_bytes()) + if actual != expected_sha256: + raise ValueError('Feature media hash mismatch: ' + relative_path) + + +def _runtime_item(item, static): + required = {'position', 'source_id', 'title', 'source_url', 'poster_path', + 'poster_sha256', 'release_context', 'description', 'featuring'} + missing = sorted(required - item.keys()) + if missing: + raise ValueError('Feature item is missing: ' + ', '.join(missing)) + if not re.fullmatch(r'tt\d+', item['source_id']): + raise ValueError('Feature item has an invalid IMDb title ID') + if not item['title'].strip() or not item['description'].strip(): + raise ValueError('Feature item title and description are required') + if not _official_imdb_url(item['source_url']): + raise ValueError('Feature item source must be an official IMDb URL') + _validate_media(static, item['poster_path'], item['poster_sha256']) + if item.get('backdrop_path'): + _validate_media(static, item['backdrop_path'], item['backdrop_sha256']) + runtime = deepcopy(item) + for key in ('poster_sha256', 'poster_source_url', 'backdrop_sha256', + 'backdrop_source_url'): + runtime.pop(key, None) + return runtime + + +def import_snapshot(snapshot_path, database, static): + snapshot_bytes = Path(snapshot_path).read_bytes() + snapshot = json.loads(snapshot_bytes) + if not _official_imdb_url(snapshot.get('source_url', '')): + raise ValueError('Expected an official IMDb editorial source URL') + if not re.fullmatch(r'[a-z0-9-]+', snapshot.get('feature_id', '')): + raise ValueError('Invalid feature ID') + if not snapshot.get('page_title', '').strip() or not snapshot.get('page_subtitle', '').strip(): + raise ValueError('Feature page title and subtitle are required') + _validate_media(static, snapshot['hero_image_path'], snapshot['hero_image_sha256']) + + items = [_runtime_item(item, static) for item in snapshot.get('items', [])] + if not items: + raise ValueError('Feature snapshot has no items') + expected_positions = list(range(1, len(items) + 1)) + if [item['position'] for item in items] != expected_positions: + raise ValueError('Feature item positions must be contiguous and source ordered') + ids = [item['source_id'] for item in items] + if len(ids) != len(set(ids)): + raise ValueError('Feature item IMDb IDs must be unique') + + database = Path(database) + with sqlite3.connect(database) as con: + protected = { + name: con.execute('SELECT * FROM "' + name + '" ORDER BY rowid').fetchall() + for (name,) in con.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name!='home_features'" + ) + } + row = con.execute( + 'SELECT kind,heading,subtitle,image_path,poster_path,source_url,captured_at,payload ' + 'FROM home_features WHERE id=?', (snapshot['feature_id'],) + ).fetchone() + if not row or row[0] != 'editorial': + raise ValueError('Expected an existing editorial home feature') + payload = json.loads(row[7]) + payload.update({ + 'page_title': snapshot['page_title'], + 'page_subtitle': snapshot['page_subtitle'], + 'hero_image_path': snapshot['hero_image_path'], + 'observed_at': snapshot['observed_at'], + 'item_count': len(items), + 'items': items, + }) + updated = ( + 'editorial', snapshot.get('card_title', row[1]), + snapshot.get('card_subtitle', row[2]), row[3], row[4], + snapshot['source_url'], snapshot['observed_at'], + json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(',', ':')), + ) + if updated != row: + con.execute( + 'UPDATE home_features SET kind=?,heading=?,subtitle=?,image_path=?,poster_path=?, ' + 'source_url=?,captured_at=?,payload=? WHERE id=?', + updated + (snapshot['feature_id'],), + ) + for name, before in protected.items(): + after = con.execute('SELECT * FROM "' + name + '" ORDER BY rowid').fetchall() + if after != before: + raise ValueError('Protected table changed: ' + name) + + return { + 'snapshot_sha256': digest(snapshot_bytes), + 'observed_at': snapshot['observed_at'], + 'feature_id': snapshot['feature_id'], + 'items': len(items), + 'protected_tables_unchanged': sorted(protected), + 'seed_sha256': digest(database.read_bytes()), + } + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('snapshot', type=Path) + parser.add_argument('database', type=Path) + parser.add_argument('static', type=Path) + parser.add_argument('--manifest', type=Path, required=True) + args = parser.parse_args() + result = import_snapshot(args.snapshot, args.database, args.static) + args.manifest.write_text(json.dumps(result, indent=2) + '\n') + print(json.dumps(result, indent=2)) diff --git a/sites/imdb/seed_homepage.py b/sites/imdb/seed_homepage.py new file mode 100644 index 00000000..8d8345ac --- /dev/null +++ b/sites/imdb/seed_homepage.py @@ -0,0 +1,335 @@ +"""Import a browser-saved IMDb webarchive into the HF-managed seed. + +Offline, explicit build-time migration; never imported at app startup. Only the +home_features table is changed. Existing catalog and benchmark state stay intact. +Assets are extracted from the archive; --fetch-missing permits fetching exact +image URLs present in its HTML, restricted to IMDb's public media CDN. +""" +import argparse +import hashlib +from html.parser import HTMLParser +import json +from pathlib import Path +import plistlib +import re +import sqlite3 +from urllib.error import URLError +from urllib.parse import urlsplit +from urllib.request import urlopen + + +class SourcePage(HTMLParser): + def __init__(self, html): + super().__init__() + self.in_data = False + self.data_parts = [] + self.images = [] + self.episodes = [] + self.episode_details = [] + self.episode_detail = None + self.stack = [] + self.cards = [] + self.card = None + self.service = '' + self.feed(html) + + def handle_starttag(self, tag, attrs): + attrs = dict(attrs) + if tag not in {'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', + 'link', 'meta', 'param', 'source', 'track', 'wbr'}: + self.stack.append((tag, attrs)) + classes = attrs.get('class', '').split() + if attrs.get('role') == 'tabpanel' and any(c.startswith('EpisodeRatingCard_card__') for c in classes): + self.episode_detail = {'depth': len(self.stack), 'image': '', 'info': '', + 'plot': '', 'title': '', 'source_path': ''} + if self.episode_detail: + if tag == 'img': + self.episode_detail['image'] = attrs.get('src', '') + if tag == 'a' and re.match(r'^/title/tt\d+/', attrs.get('href', '')): + self.episode_detail['source_path'] = attrs['href'].split('?')[0] + kind = None + if attrs.get('data-testid') == 'name-born-today-card': + kind = 'birthday' + elif 'ipc-poster-card' in classes: + if 'streaming-picks-title' in classes: + kind = 'streaming' + elif any(a.get('data-testid') == 'rttv-parent' for _, a in self.stack): + kind = 'tv_schedule' + if kind: + if self.card: + raise ValueError('Unexpected nested source card') + self.card = {'kind': kind, 'depth': len(self.stack), 'heading': '', + 'rating': '', 'age': '', 'episode': '', 'air_date': '', + 'image': '', 'source_path': '', 'episode_path': ''} + if self.card: + if tag == 'img' and not self.card['image']: + self.card['image'] = attrs.get('src', '') + if tag == 'a': + href = attrs.get('href', '') + if re.match(r'^/(title/tt|name/nm)\d+/', href) and not self.card['source_path']: + self.card['source_path'] = href.split('?')[0] + if attrs.get('data-testid') == 'rttv-episode-num': + self.card['episode_path'] = href.split('?')[0] + if tag == 'script' and attrs.get('id') == '__NEXT_DATA__': + self.in_data = True + if tag == 'img': + self.images.append(attrs) + label = attrs.get('aria-label', '') + episode = re.fullmatch(r'Episode (\d+) of \d+, rated ([\d.]+)/10: (.+)\. Select enter.*', label) + if attrs.get('role') == 'tab' and episode: + self.episodes.append({'number': int(episode[1]), 'rating': episode[2], 'title': episode[3]}) + + def handle_endtag(self, tag): + if tag == 'script': + self.in_data = False + for index in range(len(self.stack) - 1, -1, -1): + if self.stack[index][0] == tag: + if self.episode_detail and index < self.episode_detail['depth']: + self.episode_detail.pop('depth') + self.episode_details.append(self.episode_detail) + self.episode_detail = None + if self.card and index < self.card['depth']: + self.card.pop('depth') + self.cards.append(self.card) + self.card = None + del self.stack[index:] + break + + def handle_data(self, data): + if self.in_data: + self.data_parts.append(data) + if self.episode_detail: + for _, attrs in reversed(self.stack): + classes = attrs.get('class', '').split() + field = next((value for prefix, value in [ + ('EpisodeRatingCard_plot__', 'plot'), ('EpisodeRatingCard_episodeInfo__', 'info')] + if any(c.startswith(prefix) for c in classes)), None) + if 'ipc-title__text' in classes: + field = 'title' + if field: + self.episode_detail[field] += data.strip() + ' ' + break + if any(a.get('data-testid') == 'streaming-picks-tab-container' for _, a in self.stack): + if any(a.get('role') == 'tab' and a.get('aria-selected') == 'true' for _, a in self.stack): + self.service += data.strip() + if not self.card: + return + fields = {'born-today-name': 'heading', 'born-today-age': 'age', + 'rttv-episode-num': 'episode', 'rttv-air-date': 'air_date'} + for _, attrs in reversed(self.stack): + field = fields.get(attrs.get('data-testid')) + classes = attrs.get('class', '').split() + if 'ipc-poster-card__title' in classes: + field = 'heading' + elif 'ipc-rating-star--rating' in classes: + field = 'rating' + if field: + self.card[field] += data + break + + +def digest(data): + return hashlib.sha256(data).hexdigest() + + +def starmeter_entries(props): + """Use the chart's explicit ranks, never the order of a local catalog.""" + entries, ranks, ids = [], set(), set() + for edge in props.get('pageData', {}).get('chartNames', {}).get('edges', []): + node, rank = edge['node'], edge.get('currentRank') + source_id = node['id'] + if type(rank) is not int or rank < 1 or rank in ranks: + raise ValueError('Missing, invalid or duplicate STARmeter rank') + if not re.fullmatch(r'nm\d+', source_id) or source_id in ids: + raise ValueError('Invalid or duplicate STARmeter name ID') + ranks.add(rank) + ids.add(source_id) + entries.append({ + 'source_id': source_id, 'rank': rank, 'heading': node['nameText']['text'], + 'image': (node.get('primaryImage') or {}).get('url', ''), + 'professions': [p['profession']['text'] for p in node.get('professions', [])], + 'known_for': [c['title']['titleText']['text'] + for c in (node.get('knownForV2') or {}).get('credits', [])], + }) + return entries + + +def import_archive(archive_path, database, static, fetch_missing=False): + archive_bytes = Path(archive_path).read_bytes() + archive = plistlib.loads(archive_bytes) + if urlsplit(archive['WebMainResource']['WebResourceURL']).hostname != 'www.imdb.com': + raise ValueError('Expected an official IMDb browser archive') + page = SourcePage(archive['WebMainResource']['WebResourceData'].decode('utf-8')) + props = json.loads(''.join(page.data_parts))['props']['pageProps'] + captured = props['requestContext']['timestamp'] + resources = {r['WebResourceURL']: r for r in archive.get('WebSubresources', [])} + manifest = [] + static = Path(static) + downloaded = {} + + def image(source): + if not source: + return '' + prefix = source.split('._')[0] + candidates = [r for url, r in resources.items() + if url.split('._')[0] == prefix and r['WebResourceMIMEType'].startswith('image/')] + if candidates: + resource = max(candidates, key=lambda r: len(r['WebResourceData'])) + url, data = resource['WebResourceURL'], resource['WebResourceData'] + else: + matches = [img for img in page.images if img.get('src', '').split('._')[0] == prefix] + if not fetch_missing: + raise ValueError('Image not saved in archive: ' + source) + img = matches[0] if matches else {'src': source} + # srcset commas also occur inside IMDb crop parameters. Width + # descriptors delimit entries; splitting on every comma is invalid. + urls = re.findall(r'(https://\S+) \d+w', img.get('srcset', '')) + url = urls[-1] if urls else img['src'] + if urlsplit(url).hostname != 'm.media-amazon.com': + raise ValueError('Unexpected media host') + cache = static.parent / 'scraped_data/homepage-media' / digest(url.encode()) + if cache.exists(): + data = cache.read_bytes() + elif url in downloaded: + data = downloaded[url] + else: + for attempt in range(2): + try: + with urlopen(url, timeout=30) as response: + if not response.headers.get('Content-Type', '').startswith('image/'): + raise ValueError('Expected an image') + data = response.read(8_000_001) + break + except URLError as exc: + if attempt: + raise ValueError('Could not retrieve source image: ' + url) from exc + if len(data) <= 8_000_000 and data.startswith((b'\xff\xd8\xff', b'\x89PNG')): + cache.parent.mkdir(parents=True, exist_ok=True) + cache.write_bytes(data) + downloaded[url] = data + if len(data) > 8_000_000: + raise ValueError('Image exceeds import budget') + if not data.startswith((b'\xff\xd8\xff', b'\x89PNG')): + raise ValueError('Expected a JPEG or PNG') + filename = 'images/home/' + digest(data)[:24] + ('.png' if data.startswith(b'\x89PNG') else '.jpg') + destination = static / filename + destination.parent.mkdir(parents=True, exist_ok=True) + if destination.exists() and destination.read_bytes() != data: + raise ValueError('Asset path collision') + if not destination.exists(): + destination.write_bytes(data) + manifest.append({'path': filename, 'source_url': url, 'sha256': digest(data), 'bytes': len(data)}) + return filename + + rows = [] + for key, placement in props.get('cmsContext', {}).get('transformedPlacements', {}).items(): + args = placement.get('transformedArguments', {}) + if key.startswith('hero-video-'): + payload = {'title_id': args['titleId'], 'title': args['titleText'], + 'duration': args['runtime'], 'source_cta': args['subHeadline']} + rows.append((key, 'hero', int(key.rsplit('-', 1)[1]), args['headline'], + 'Trailer preview', image(args['videoSlateImage']['url']), + image(args['posterImage']['url']), 'https://www.imdb.com/video/' + args['videoId'] + '/', + captured, json.dumps(payload))) + elif key.startswith('featured-today-'): + rows.append((key, 'editorial', int(key.rsplit('-', 1)[1]), args['displayTitle'], + args.get('description', ''), image(args['linkedImages'][0]['imageModel']['url']), + '', 'https://www.imdb.com' + args['linkTargetUrl'], captured, + json.dumps({'source_cta': args.get('callToActionText', ''), 'type': args.get('iconName', '')}))) + elif key.startswith('pill-'): + rows.append((key, 'topic', int(key.rsplit('-', 1)[1]), args['displayTitle'], '', '', '', + 'https://www.imdb.com' + args['linkTargetUrl'], captured, '{}')) + elif key == 'center-1': + spec = json.loads(args['json']) + title = args['transformedJson'] + episodes = [] + for episode in page.episodes: + detail = next((d for d in page.episode_details + if re.match(r'S\d+\.E' + str(episode['number']) + r'\s', d['info'])), None) + enriched = dict(episode) + if detail: + if detail['title'].strip() != episode['title']: + raise ValueError('Episode label and detail title disagree') + enriched.update(info=detail['info'].strip(), plot=detail['plot'].strip(), + image_path=image(detail['image']), + source_url='https://www.imdb.com' + detail['source_path']) + episodes.append(enriched) + rows.append((key, 'spotlight', 1, spec['title'], spec['description'], + '', image(title['primaryImage']['url']), + 'https://www.imdb.com/title/' + title['id'] + '/episodes/', captured, + json.dumps({'title_id': title['id'], 'title': title['titleText']['text'], + 'season': title['episodes']['episodes']['edges'][0]['node']['series']['displayableEpisodeNumber']['displayableSeason']['season'], + 'episodes': episodes, + 'credits': [{'role': c['category']['text'], 'names': [ + n['name']['nameText']['text'] for n in c['credits']]} + for c in title['principalCredits']]}))) + # Keep the benchmark news table untouched; this snapshot has its own links. + for position, edge in enumerate(props.get('pageQueryData', {}).get('data', {}).get('news', {}).get('edges', [])): + item = edge['node'] + rows.append((item['id'], 'news', position, item['articleTitle']['plainText'], + item['text']['plainText'], image(item['image']['url']), '', 'https://www.imdb.com/news/' + item['id'] + '/', + captured, json.dumps({'date': item['date'], 'source': item['source']['homepage']['label']}))) + for entry in starmeter_entries(props): + payload = {key: value for key, value in entry.items() if key not in {'heading', 'image'}} + rows.append(('starmeter-' + entry['source_id'], 'starmeter', entry['rank'], + entry['heading'], ' · '.join(entry['professions']), image(entry['image']), '', + 'https://www.imdb.com/name/' + entry['source_id'] + '/', captured, json.dumps(payload))) + positions = {} + for card in page.cards: + kind = card['kind'] + if not card['heading'].strip() or not card['source_path'] or not card['image']: + raise ValueError('Incomplete loaded homepage card: ' + str(card)) + if kind == 'streaming' and not page.service: + raise ValueError('Streaming cards require an observed selected service') + positions[kind] = positions.get(kind, 0) + 1 + source_id = card['source_path'].strip('/').split('/')[-1] + payload = {k: v.strip() for k, v in card.items() + if k not in {'kind', 'heading', 'image', 'source_path'} and v} + payload['source_id'] = source_id + if kind == 'streaming': + payload['service'] = page.service + subtitle = {'birthday': 'Born on ' + captured[5:10], + 'streaming': page.service + ' · availability at capture', + 'tv_schedule': ' · '.join(filter(None, [card['episode'], card['air_date']]))}[kind] + rows.append((kind + '-' + source_id, kind, positions[kind], card['heading'].strip(), + subtitle, image(card['image']) if kind == 'birthday' else '', + image(card['image']) if kind != 'birthday' else '', + 'https://www.imdb.com' + card['source_path'], captured, json.dumps(payload))) + if not rows: + raise ValueError('No supported source content found') + columns = 'id,kind,position,heading,subtitle,image_path,poster_path,source_url,captured_at,payload' + with sqlite3.connect(database) as con: + before = {name: con.execute('SELECT * FROM "' + name + '" ORDER BY rowid').fetchall() + for (name,) in con.execute("SELECT name FROM sqlite_master WHERE type='table' AND name!='home_features'")} + con.execute("""CREATE TABLE IF NOT EXISTS home_features ( + id VARCHAR(80) PRIMARY KEY, kind VARCHAR(30) NOT NULL, position INTEGER NOT NULL, + heading TEXT NOT NULL, subtitle TEXT, image_path TEXT, poster_path TEXT, + source_url TEXT NOT NULL, captured_at VARCHAR(40) NOT NULL, payload JSON NOT NULL)""") + # An archive saved before lazy loading must not erase previously sourced + # lower-page collections. Replace only the kinds actually present here. + kinds = sorted({row[1] for row in rows}) + placeholders = ','.join('?' for _ in kinds) + current = con.execute('SELECT ' + columns + ' FROM home_features WHERE kind IN (' + + placeholders + ') ORDER BY id', kinds).fetchall() + if current != sorted(rows): + con.execute('DELETE FROM home_features WHERE kind IN (' + placeholders + ')', kinds) + con.executemany('INSERT INTO home_features (' + columns + ') VALUES (?,?,?,?,?,?,?,?,?,?)', rows) + for name, values in before.items(): + if values != con.execute('SELECT * FROM "' + name + '" ORDER BY rowid').fetchall(): + raise ValueError('Protected table changed: ' + name) + return {'archive_sha256': digest(archive_bytes), 'captured_at': captured, 'features': len(rows), + 'assets': manifest, 'protected_tables_unchanged': list(before), + 'seed_sha256': digest(Path(database).read_bytes())} + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('archive', type=Path) + parser.add_argument('database', type=Path) + parser.add_argument('static', type=Path) + parser.add_argument('--fetch-missing', action='store_true') + parser.add_argument('--manifest', type=Path, required=True) + args = parser.parse_args() + result = import_archive(args.archive, args.database, args.static, args.fetch_missing) + args.manifest.write_text(json.dumps(result, indent=2) + '\n') + print(json.dumps({k: v for k, v in result.items() if k != 'assets'}, indent=2)) diff --git a/sites/imdb/static/css/.gitkeep b/sites/imdb/static/css/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/imdb/static/css/home.css b/sites/imdb/static/css/home.css new file mode 100644 index 00000000..5a3cfda6 --- /dev/null +++ b/sites/imdb/static/css/home.css @@ -0,0 +1,354 @@ +/* Homepage snapshot and shared navigation. Catalog detail styles remain in style.css. */ +[hidden] { display: none !important; } +.sr-only, .skip-link:not(:focus) { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; } +.skip-link:focus { position: fixed; left: 16px; top: 12px; z-index: 100; background: #fff; padding: 12px; } +button, summary { -webkit-tap-highlight-color: transparent; } +.brand { padding: 0; background: transparent; } +.brand img { display: block; width: 100%; height: 100%; } +.menu-toggle, .mobile-search { border: 0; background: transparent; color: #fff; display: flex; align-items: center; gap: 6px; font: inherit; font-size: 14px; font-weight: 700; padding: 8px; cursor: pointer; border-radius: 4px; } +.menu-toggle > span:first-child { font-size: 21px; line-height: 1; } +.menu-toggle:hover, .mobile-search:hover { background: #333; } +.imdb-pro { color: #fff; font-weight: 800; white-space: nowrap; font-size: 17px; } +.imdb-pro span { color: #55b9d6; } +.language-menu { position: relative; font-size: 14px; } +.language-menu summary { list-style: none; cursor: pointer; white-space: nowrap; } +.language-menu p { position: absolute; z-index: 10; right: 0; top: 20px; width: 230px; padding: 20px; color: #fff; background: #222; border: 1px solid #555; } +.mobile-search, .use-app { display: none; } +.topbar .nav { gap: 12px; } +.topbar .user-area { gap: 16px; } +.account-link { max-width: 130px; overflow: hidden; text-overflow: ellipsis; } +#nav-drawer { max-width: none; max-height: none; width: 100%; height: 100%; margin: 0; border: 0; padding: 24px; background: #1f1f1f; color: #fff; } +#nav-drawer::backdrop { background: #000b; } +.drawer-inner { max-width: 1050px; margin: auto; } +.drawer-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 48px; } +.drawer-head .brand { flex-basis: 96px; height: 48px; font-size: 36px; line-height: 48px; } +.drawer-head form { margin: 0; } +.close-dialog { border: 0; border-radius: 50%; background: #f5c518; color: #111; font-size: 36px; width: 48px; height: 48px; cursor: pointer; line-height: 1; } +.menu-columns { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 36px 56px; } +.menu-columns h2 { font-size: 23px; border: 0; padding: 0; margin: 0 0 16px; display: block; } +.menu-columns a { display: block; color: #fff; margin: 14px 0; } +.home-page main.container { padding-top: 8px; padding-bottom: 24px; } +.home-page h2 { color: #fff; font-size: 26px; margin: 0 0 12px; border-radius: 2px; } +.home-page h2 a { color: inherit; } +.home-page h3 { font-size: 17px; font-weight: 500; line-height: 1.4; } +.home-page .section-yellow { color: #f5c518; border: 0; padding: 0; font-size: 32px; font-weight: 600; margin-bottom: 28px; display: block; } +.home-section { margin: 36px 0 48px; min-width: 0; } +.section-caption { color: #aaa; margin: 0 0 20px; font-size: 16px; } +.text-link { color: #5799ef; } +.home-hero { display: grid; grid-template-columns: minmax(0,2.08fr) minmax(0,1fr); gap: 16px; } +.hero-stage { position: relative; min-width: 0; } +.hero-slide { position: relative; padding-bottom: 8px; } +.hero-media { display: block; position: relative; } +.hero-media::after { content: ''; position: absolute; inset: 0; background: linear-gradient(transparent 45%, #000 98%); pointer-events: none; } +.hero-media img { display: block; width: 100%; aspect-ratio: 16/9; object-fit: cover; border-radius: 8px 8px 0 0; } +.hero-caption { position: relative; display: flex; align-items: flex-end; gap: 16px; margin: -22% 16px 0; } +.hero-poster { flex: 0 0 20%; } +.hero-poster img { width: 100%; aspect-ratio: 27/40; object-fit: cover; border-radius: 0 10px 10px 10px; display: block; } +.hero-description { display: flex; align-items: center; gap: 16px; color: #fff; min-width: 0; padding: 0 0 8px; } +.hero-description:hover { text-decoration: none; } +.hero-description:hover .play-circle { color: #f5c518; border-color: #f5c518; } +.hero-description h1 { font-size: 27px; font-weight: 400; line-height: 1.2; margin-bottom: 8px; } +.hero-description small { font-size: 18px; color: #bbb; white-space: nowrap; padding-left: 8px; } +.hero-description p { margin: 0; color: #bbb; } +.media-availability { display: block; color: #999; font-size: 11px; margin-top: 8px; } +.play-circle { width: 64px; height: 64px; border: 2px solid #fff; border-radius: 50%; display: grid; place-items: center; flex: none; font-size: 38px; } +.hero-arrow, .rail-next { position: absolute; background: #121212a6; border: 1px solid #aaa; border-radius: 4px; color: white; font-size: 40px; line-height: 1; width: 48px; height: 64px; cursor: pointer; z-index: 1; } +.hero-arrow:hover, .rail-next:hover { color: #f5c518; } +.hero-arrow { top: 30%; } +.hero-arrow.previous { left: 0; } +.hero-arrow.next { right: 0; } +.up-next h2 { color: #f5c518; font-size: 20px; border: 0; padding: 0; margin: 0 0 12px; } +.up-next-list { display: flex; flex-direction: column; background: linear-gradient(#161616, #000); padding: 16px; } +.up-next-list > a { display: flex; align-items: center; gap: 16px; margin-bottom: 16px; color: #fff; } +.up-next-list > a:last-child { margin-bottom: 0; } +.up-next-list img { width: 84px; aspect-ratio: 27/40; object-fit: cover; border-radius: 8px; } +.up-next-list h3 { font-size: 17px; margin: 6px 0 2px; } +.up-next-list p { font-size: 14px; color: #aaa; margin: 0; } +.up-next-runtime { color: #ddd; font-size: 14px; } +.browse-trailers { display: block; padding: 16px; font-size: 20px; font-weight: 600; color: #fff; } +.topic-chips { display: flex; gap: 8px; overflow: auto; padding: 12px 0; scrollbar-width: thin; } +.topic-chips a { border: 1px solid #666; border-radius: 30px; padding: 4px 14px; white-space: nowrap; color: inherit; font-size: 14px; } +.topic-chips a:hover { background: #8883; } +.home-topics { border-top: 1px solid #333; margin-top: 16px; padding: 20px 0 0; } +.rail-wrap { position: relative; min-width: 0; } +[data-rail] { scrollbar-width: none; overscroll-behavior-x: contain; scroll-snap-type: x proximity; } +[data-rail]::-webkit-scrollbar { display: none; } +[data-rail] > * { scroll-snap-align: start; } +.rail-next { right: -10px; top: 32%; } +.editorial-rail { display: grid; grid-auto-flow: column; grid-auto-columns: calc((100% - 32px) / 3); gap: 16px; overflow-x: auto; } +.editorial-rail article { min-width: 0; } +.editorial-rail article > a:first-child { color: #fff; } +.editorial-rail h3 { margin: 12px 0 6px; } +.editorial-image { position: relative; } +.editorial-image img { width: 100%; aspect-ratio: 16/9; object-fit: cover; display: block; border-radius: 8px; } +.editorial-image span { position: absolute; bottom: 0; left: 0; right: 0; padding: 26px 12px 10px; background: linear-gradient(transparent, #000b); color: #fff; } +.spotlight { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,2fr); gap: 24px; } +.home-page .spotlight h2 { font-size: 23px; } +.spotlight-body { display: flex; gap: 16px; align-items: flex-start; } +.spotlight-body > img { width: 110px; aspect-ratio: 27/40; object-fit: cover; border-radius: 8px; } +.spotlight-body h3 { font-size: 18px; margin: 0; } +.spotlight-body p, .spotlight-body .text-link { font-size: 14px; } +.spotlight-body strong { display: block; margin-bottom: 4px; color: #aaa; } +.spotlight-body p { color: #5799ef; } +.spotlight-episodes { min-width: 0; } +.episode-preview { display: grid; grid-template-columns: minmax(0,1.5fr) minmax(0,1fr); background: #181818; border-radius: 8px; overflow: hidden; } +.episode-preview > img { width: 100%; height: 100%; min-height: 210px; object-fit: cover; } +.episode-preview > div { padding: 16px; } +.episode-preview p { font-size: 13px; margin: 8px 0; } +.episode-preview .episode-info { font-size: 11px; color: #aaa; margin-top: 0; } +.episode-preview h3 { margin: 0; } +.episode-preview a { color: #fff; } +.episode-detail { margin: 32px 0; scroll-margin-top: 80px; } +.episode-detail > img { width: 100%; max-width: 480px; border-radius: 8px; } +.people-rail { display: grid; grid-auto-flow: column; grid-auto-columns: calc((100% - 80px) / 6); gap: 16px; overflow: auto; } +.people-rail a { text-align: center; color: white; } +.people-rail img { display: block; width: 100%; aspect-ratio: 1; object-fit: cover; object-position: center 25%; border-radius: 50%; } +.people-rail h3 { font-size: 16px; } +.ranked-person { display: block; text-align: center; color: #fff; min-width: 0; } +.ranked-person img, .portrait-unavailable { display: block; width: 100%; aspect-ratio: 1; object-fit: cover; object-position: center 25%; border-radius: 50%; } +.portrait-unavailable { background: #171717; padding: 24px; font-size: 14px; color: #aaa; } +.person-rank { display: block; color: #aaa; font-size: 16px; margin-top: 16px; } +.ranked-person h3 { margin: 6px 0 0; } +.people-grid { display: grid; grid-template-columns: repeat(6,minmax(0,1fr)); gap: 32px 24px; } +.title-rail { list-style: none; padding: 0; margin: 0; display: grid; grid-auto-flow: column; grid-auto-columns: calc((100% - 80px) / 6); gap: 16px; overflow-x: auto; } +.title-card { background: #1a1a1a; border-radius: 10px; overflow: hidden; min-width: 0; } +.title-card-poster { position: relative; } +.title-card-poster img, .poster-unavailable { width: 100%; aspect-ratio: 27/40; display: block; object-fit: cover; } +.poster-unavailable { padding: 20px; color: #aaa; } +.watchlist-ribbon { position: absolute; top: 0; left: 0; background: #111a; border: 0; color: #fff; font: inherit; font-size: 28px; width: 36px; height: 48px; text-align: center; line-height: 38px; clip-path: polygon(0 0,100% 0,100% 100%,50% 80%,0 100%); cursor: pointer; } +.watchlist-ribbon:hover { background: #555c; text-decoration: none; } +.title-card-content { padding: 10px 12px; } +.title-card-rating { display: flex; align-items: center; gap: 24px; color: #bbb; } +.title-card-rating > a { color: #5799ef; font-size: 24px; padding: 0 8px; } +.title-card-name { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; color: #fff; font-size: 16px; height: 48px; margin: 8px 0 16px; } +.title-card form { margin: 0; } +.card-watchlist { border: 0; display: block; width: 100%; text-align: center; padding: 9px 2px; background: #2c2c2c; color: #5799ef; font: inherit; font-size: 14px; font-weight: 600; border-radius: 24px; cursor: pointer; white-space: nowrap; } +.card-watchlist:hover { background: #393939; text-decoration: none; } +.card-bottom { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-top: 12px; font-size: 13px; } +.card-bottom a { color: #fff; } +.card-bottom a:last-child { font-size: 22px; } +.watchlist-empty { text-align: center; padding: 32px 0 12px; } +.bookmark-large { display: block; width: 42px; height: 56px; margin: 0 auto 20px; font-size: 38px; line-height: 48px; background: #333; clip-path: polygon(0 0,100% 0,100% 100%,50% 80%,0 100%); } +.watchlist-empty p { margin: 8px 0 24px; } +.sign-in-button { background: #f5c518; color: #111; display: inline-block; border-radius: 24px; font-weight: 600; padding: 8px 32px; } +.footer .sign-in-button { color: #111; } +.interest-grid { display: flex; gap: 16px; overflow-x: auto; } +.interest-grid a { background: #171717; color: #fff; border-radius: 12px; border: 1px solid #444; min-width: 180px; padding: 32px 20px; font-size: 20px; } +.interest-grid span { float: right; padding-left: 24px; } +.service-tabs { display: flex; gap: 24px; border-bottom: 1px solid #333; margin-bottom: 20px; } +.service-tabs a { color: #fff; font-size: 14px; font-weight: 700; padding: 12px 8px; } +.service-tabs a:first-child { border-bottom: 3px solid #f5c518; } +.explore-links { display: flex; gap: 20px; margin: 24px 0; flex-wrap: wrap; } +.explore-links a { padding: 20px; border: 1px solid #444; border-radius: 10px; flex: 1 1 230px; color: inherit; font-size: 20px; } +.home-news-layout { display: grid; grid-template-columns: minmax(0,2fr) minmax(0,1fr); gap: 32px; } +.home-news-layout aside { min-width: 0; border-left: 1px solid #333; padding-left: 24px; } +.home-news-grid { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 24px; align-content: start; } +.home-news-grid article:first-child { grid-column: 1 / -1; } +.home-news-grid article { min-width: 0; } +.home-news-grid a { color: #fff; display: flex; align-items: flex-start; gap: 16px; } +.home-news-grid p { font-size: 12px; margin: 8px 0 0; } +.home-news-grid h3 { font-size: 15px; margin: 0; } +.home-news-grid article:first-child h3 { font-size: 18px; } +.home-news-grid .news-excerpt { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; color: #ddd; font-size: 14px; } +.home-page .news-list { color: #aaa; font-size: 14px; } +.recently-viewed { border-top: 1px solid #333; padding-top: 32px; } +.text-button { background: transparent; border: 0; font: inherit; color: #5799ef; cursor: pointer; } +.recent-rail { display: flex; gap: 16px; overflow: auto; } +.recent-rail img { display: block; width: 88px; aspect-ratio: 27/40; object-fit: cover; border-radius: 8px; } +.footer { font-size: 13px; } +.footer-panels { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 24px; max-width: 820px; margin: 32px auto; } +.footer-panels section { padding: 20px; border: 1px solid #444; border-radius: 14px; color: #fff; } +.footer-panels h3 { margin: 0 0 12px; font-size: 20px; } +.footer-panels p { margin: 0 0 10px; color: #aaa; } +.social-links { display: flex; justify-content: center; gap: 18px; flex-wrap: wrap; } +.social-links a, .footer-panels a { color: #fff; } +.footer-links { max-width: 870px; margin: 0 auto 24px; } +.footer-brand { color: #fff; } +.footer-brand strong { font-size: 20px; } +.offline-note { max-width: 760px; margin: 24px auto 12px; font-size: 11px; } +.feature-detail { max-width: 900px; margin: 20px auto 60px; } +.feature-wide { width: 100%; display: block; border-radius: 8px; margin: 24px 0; } +.feature-poster { max-width: 180px; margin: 24px 0; } +.source-provenance { margin: 24px 0; color: #aaa; overflow-wrap: anywhere; } +.news-article { max-width: 800px; margin: 24px auto; } +.home-news-grid img { width: 60px; height: 89px; flex: none; object-fit: cover; border-radius: 8px; } +.home-news-grid article:first-child img { width: 90px; height: 133px; } +.feature-news-image { width: 160px; max-height: 220px; object-fit: contain; margin: 20px 0; } +.season-label { color: #aaa; margin: 0 0 12px; } +.episode-bars { display: flex; align-items: flex-end; gap: 12px; padding: 12px 0; margin-bottom: 12px; border-bottom: 1px solid #333; } +.episode-bars button { display: flex; flex-direction: column; align-items: center; flex: 1; gap: 6px; color: #bbb; min-width: 0; font: inherit; font-size: 13px; padding: 0 0 8px; border: 0; border-bottom: 2px solid transparent; background: transparent; cursor: pointer; } +.episode-bar { display: block; width: 100%; max-width: 48px; border-radius: 4px 4px 0 0; background: #a08ff4; } +.episode-bars button:hover .episode-bar { background: #c8bcff; } +.episode-bars button[aria-pressed="true"] { color: #f5c518; border-color: #f5c518; } +.episode-score { color: #fff; font-size: 14px; } +.snapshot-poster { display: block; } +.snapshot-poster img { display: block; width: 100%; aspect-ratio: 27/40; object-fit: cover; } +.snapshot-card { display: flex; flex-direction: column; } +.snapshot-card .title-card-content { display: flex; flex-direction: column; flex: 1; } +.snapshot-card .title-card-rating { font-size: 14px; min-height: 24px; } +.snapshot-card .card-watchlist { margin-top: auto; } +.schedule-meta { display: flex; flex-direction: column; gap: 4px; font-size: 14px; color: #aaa; margin: -6px 0 18px; } +.schedule-meta span:first-child { color: #5799ef; } +.person-age { display: block; font-size: 16px; color: #aaa; margin-top: 4px; } +.snapshot-date { color: #aaa; font-size: 14px; margin: 16px 0 24px; } +.snapshot-grid { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(6, minmax(0,1fr)); gap: 24px 16px; } +.feature-portrait { width: 220px; aspect-ratio: 1; object-fit: cover; border-radius: 50%; margin: 20px 0; } +.snapshot-facts { display: grid; grid-template-columns: max-content minmax(0,1fr); gap: 12px 24px; margin: 28px 0; } +.snapshot-facts dt { color: #aaa; } +.snapshot-facts dd { margin: 0; } +.box-office-grid { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); grid-auto-flow: column; grid-template-rows: repeat(3,auto); gap: 24px 40px; } +.box-office-grid li { display: flex; align-items: center; gap: 16px; min-width: 0; } +.box-rank { width: 20px; text-align: center; border-right: 2px solid #f5c518; padding-right: 12px; } +.box-poster img { width: 48px; aspect-ratio: 27/40; object-fit: cover; display: block; border-radius: 4px; } +.box-office-grid li > div { display: flex; flex-direction: column; gap: 6px; } +.box-office-grid a { color: #fff; } +.box-office-grid li > div > span { color: #aaa; font-size: 14px; } +@media (max-width: 1023px) { + .people-grid { grid-template-columns: repeat(4,minmax(0,1fr)); } + .spotlight { grid-template-columns: 1fr; } + .home-news-layout { grid-template-columns: 1fr; } + .home-news-layout aside { border-left: 0; border-top: 1px solid #333; padding: 16px 0 0; } + .snapshot-grid { grid-template-columns: repeat(4,minmax(0,1fr)); } + .home-hero { display: block; } + .up-next { display: none; } + .topbar .nav { flex-wrap: nowrap; padding-top: 0; padding-bottom: 0; } + .topbar .search { order: initial; flex-basis: auto; } + .imdb-pro, .language-menu, .user-area .nav-watchlist { display: none; } + .title-rail, .people-rail { grid-auto-columns: calc((100% - 48px) / 4); } + .editorial-rail { grid-auto-columns: calc((100% - 16px) / 2); } + .hero-description h1 { font-size: 28px; } +} +@media (max-width: 600px) { + .people-grid { grid-template-columns: repeat(3,minmax(0,1fr)); gap: 24px 12px; } + .snapshot-grid { grid-template-columns: repeat(2,minmax(0,1fr)); gap: 20px 10px; } + .box-office-grid { grid-template-columns: 1fr; grid-auto-flow: row; gap: 20px; } + .snapshot-facts { grid-template-columns: 1fr; gap: 4px; } + .snapshot-facts dd { margin-bottom: 12px; } + .topbar .nav { min-height: 56px; gap: 4px; flex-wrap: wrap; position: relative; } + .topbar .brand { flex: 0 0 56px; font-size: 23px; height: 29px; line-height: 29px; } + .menu-toggle { order: -1; padding: 8px 6px; } + .menu-label { display: none; } + .mobile-search { display: flex; margin-left: auto; padding: 8px; } + .topbar .user-area { margin: 0 4px; gap: 8px; font-size: 13px; } + .account-link { max-width: 80px; } + .user-area .logout-form { display: none; } + .use-app { display: block; border-radius: 20px; background: #f5c518; padding: 5px 12px; color: #000; font-size: 12px; font-weight: 600; } + .topbar .search { display: none; } + .topbar.search-open .search { display: flex; flex: 1 1 100%; order: 5; margin: 4px 4px 12px; } + #nav-drawer { padding: 20px; } + .drawer-head { margin-bottom: 24px; } + .menu-columns { grid-template-columns: 1fr; gap: 20px; } + .menu-columns section { border-bottom: 1px solid #444; } + .home-page main.container { padding-top: 0; } + .home-hero { margin: 0 -16px; } + .hero-media img { border-radius: 0; } + .hero-caption { gap: 10px; margin: -20% 12px 0; align-items: flex-end; } + .hero-poster { flex-basis: 21%; } + .hero-description { gap: 8px; padding-bottom: 2px; } + .hero-description h1 { font-size: 20px; margin-bottom: 4px; } + .hero-description small { font-size: 13px; padding-left: 0; } + .hero-description p { font-size: 14px; } + .play-circle { width: 40px; height: 40px; font-size: 24px; } + .media-availability { font-size: 9px; margin-top: 4px; } + .hero-arrow { height: 44px; width: 30px; font-size: 32px; top: 25%; } + .home-topics { margin-top: 16px; padding-top: 12px; } + .home-section { margin: 28px 0 36px; } + .home-page h2 { font-size: 23px; } + .home-page .section-yellow { font-size: 27px; margin-bottom: 20px; } + .section-caption { font-size: 14px; margin-bottom: 16px; } + .editorial-rail { grid-auto-columns: 85%; gap: 12px; } + .people-rail { grid-auto-columns: 110px; gap: 16px; } + .people-rail h3 { font-size: 14px; } + .title-rail { grid-auto-columns: 156px; gap: 10px; } + .rail-next { display: none; } + .spotlight-body { padding: 16px; gap: 16px; align-items: flex-start; } + .spotlight-body > img { width: 84px; } + .spotlight-body h3 { font-size: 20px; } + .spotlight-body p { font-size: 12px; } + .spotlight-body .text-link { font-size: 13px; } + .home-news-grid { grid-template-columns: 1fr; } + .episode-bars { gap: 8px; } + .episode-preview { grid-template-columns: 1fr; } + .episode-preview > img { height: auto; min-height: 0; aspect-ratio: 16/9; } + .explore-links { gap: 12px; } + .footer-panels { grid-template-columns: 1fr; gap: 16px; } + .footer { padding: 24px 0; } +} + +/* Sourced enhanced-list feature pages. */ +.anticipated-feature { + width: 100vw; + margin: -8px 0 -24px calc(50% - 50vw); + color: #fff; + background: + radial-gradient(circle at 50% 6%, #4aa06f 0, transparent 26rem), + linear-gradient(180deg, #327b59 0%, #295f4d 24%, #18372f 68%, #10251f 100%); +} +.anticipated-shell { max-width: 1120px; margin: 0 auto; padding: 20px 32px 72px; } +.anticipated-back { color: #fff; display: inline-flex; align-items: center; gap: 4px; font-weight: 600; padding: 10px 0; } +.anticipated-header { max-width: 980px; margin: 0 auto; padding: 54px 0 12px; text-align: center; } +.anticipated-brand { margin: 0 0 48px; font-size: 43px; line-height: .72; font-weight: 900; letter-spacing: -2px; } +.anticipated-brand span { color: #fff; font-size: 66px; padding-right: 10px; } +.anticipated-brand small { color: #f5c518; display: inline-block; margin-left: 175px; font-size: 16px; letter-spacing: 2px; } +.anticipated-header h1 { margin: 0; font-size: clamp(32px, 4vw, 50px); line-height: 1.12; font-weight: 700; } +.anticipated-header > p:not(.anticipated-brand):not(.anticipated-count) { max-width: 760px; margin: 22px auto 36px; font-size: 18px; line-height: 1.55; } +.anticipated-hero { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; object-position: center; display: block; border-radius: 12px; box-shadow: 0 22px 50px #07120e66; } +.anticipated-count { margin: 22px 0 0; color: #dbe9e2; font-size: 14px; letter-spacing: .04em; text-transform: uppercase; } +.anticipated-list { max-width: 980px; margin: 0 auto; } +.anticipated-item { display: grid; grid-template-columns: 126px minmax(0,1fr); gap: 24px; padding: 46px 0; border-top: 1px solid #ffffff36; scroll-margin-top: 72px; } +.anticipated-poster { display: block; align-self: start; border-radius: 9px; overflow: hidden; box-shadow: 0 10px 24px #06110d99; } +.anticipated-poster img { width: 100%; aspect-ratio: 140 / 207; object-fit: cover; display: block; } +.anticipated-copy { min-width: 0; } +.home-page .anticipated-copy h2 { margin: -5px 0 8px; padding: 0; border: 0; font-size: 31px; line-height: 1.2; } +.anticipated-copy h2 a { color: #fff; } +.anticipated-copy h2 span { display: inline-block; padding-left: 5px; font-size: 40px; font-weight: 400; vertical-align: -3px; } +.anticipated-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 0; margin: 0 0 18px; color: #f0f4f2; } +.anticipated-meta span { display: inline-flex; align-items: center; } +.anticipated-meta span + span::before { content: '·'; color: #bed0c8; margin: 0 9px; } +.anticipated-rating { color: #f5c518; } +.anticipated-rating small { color: #dbe4e0; font-size: inherit; font-weight: 400; margin-left: 4px; } +.anticipated-release { color: #f5c518; font-weight: 700; margin: 0 0 8px; } +.anticipated-description { margin: 0; max-width: 790px; font-size: 17px; line-height: 1.55; color: #f7faf8; } +.anticipated-actions { display: flex; flex-wrap: wrap; gap: 10px; margin: 20px 0; } +.anticipated-actions a { color: #fff; border: 1px solid #dbe9e2; border-radius: 24px; padding: 7px 17px; font-weight: 600; } +.anticipated-actions a:hover { background: #ffffff1f; text-decoration: none; } +.anticipated-cast { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } +.anticipated-cast strong { width: 100%; color: #dbe9e2; font-size: 12px; letter-spacing: .09em; text-transform: uppercase; } +.anticipated-cast a { color: #fff; border: 1px solid #ffffff5c; border-radius: 18px; padding: 4px 11px; font-size: 13px; } +.anticipated-cast a:hover { background: #ffffff1f; text-decoration: none; } +.anticipated-source { max-width: 980px; margin: 30px auto 0; padding-top: 24px; border-top: 1px solid #ffffff36; color: #c9d8d1; overflow-wrap: anywhere; } +.anticipated-back-bottom { margin-top: 22px; } +.anticipated-title-page { max-width: 980px; margin: 32px auto 72px; padding: 36px; border-radius: 14px; color: #fff; background: linear-gradient(145deg, #2e6753, #172d27); } +.anticipated-title-layout { display: grid; grid-template-columns: 220px minmax(0,1fr); gap: 36px; margin-top: 22px; align-items: start; } +.anticipated-title-layout > img { width: 100%; aspect-ratio: 140 / 207; object-fit: cover; border-radius: 10px; box-shadow: 0 14px 30px #0008; } +.anticipated-title-layout h1 { font-size: clamp(36px, 5vw, 56px); line-height: 1.05; margin: 8px 0 16px; } +.anticipated-kicker { margin: 0; color: #f5c518; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; font-size: 13px; } +.anticipated-title-page .anticipated-description { max-width: none; } +.anticipated-detail-cast { margin-top: 38px; padding-top: 28px; border-top: 1px solid #ffffff36; } +.home-page .anticipated-detail-cast h2 { margin-bottom: 18px; } + +@media (max-width: 600px) { + .anticipated-feature { margin-top: 0; } + .anticipated-shell { padding: 10px 16px 52px; } + .anticipated-header { padding: 38px 0 10px; } + .anticipated-brand { margin-bottom: 36px; font-size: 29px; letter-spacing: -1px; } + .anticipated-brand span { font-size: 45px; } + .anticipated-brand small { margin-left: 115px; font-size: 10px; } + .anticipated-header h1 { font-size: 31px; } + .anticipated-header > p:not(.anticipated-brand):not(.anticipated-count) { font-size: 16px; margin: 16px auto 28px; } + .anticipated-hero { width: calc(100% + 16px); margin-left: -8px; border-radius: 10px; } + .anticipated-count { font-size: 12px; } + .anticipated-item { display: block; overflow: auto; padding: 34px 0; } + .anticipated-poster { float: left; width: 86px; margin: 0 14px 10px 0; } + .home-page .anticipated-copy h2 { font-size: 24px; } + .anticipated-copy h2 span { font-size: 30px; } + .anticipated-meta { font-size: 14px; margin-bottom: 14px; } + .anticipated-meta span + span::before { margin: 0 6px; } + .anticipated-release, .anticipated-description { font-size: 15px; } + .anticipated-actions a { padding: 6px 12px; font-size: 13px; } + .anticipated-title-page { margin: 18px 0 48px; padding: 22px 16px; } + .anticipated-title-layout { display: block; } + .anticipated-title-layout::after { content: ''; display: block; clear: both; } + .anticipated-title-layout > img { float: left; width: 108px; margin: 0 18px 12px 0; } + .anticipated-title-layout h1 { font-size: 30px; } +} diff --git a/sites/imdb/static/css/style.css b/sites/imdb/static/css/style.css new file mode 100644 index 00000000..05ce9c06 --- /dev/null +++ b/sites/imdb/static/css/style.css @@ -0,0 +1,394 @@ +:root { + --imdb-yellow: #f5c518; + --imdb-yellow-dark: #d9ae00; + --imdb-bg: #ffffff; + --imdb-fg: #1a1a1a; + --imdb-fg-muted: #5a5a5a; + --imdb-border: #e3e3e3; + --imdb-panel: #f7f7f7; + --imdb-top-bg: #121212; + --imdb-link: #136cb2; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + background: var(--imdb-bg); + color: var(--imdb-fg); + line-height: 1.45; +} + +.container { max-width: 1180px; margin: 0 auto; padding: 0 16px; } + +/* topbar */ +.topbar { + background: var(--imdb-top-bg); + color: #fff; + padding: 10px 0; +} +.topbar .nav { display: flex; align-items: center; gap: 18px; } +.brand { + font-weight: 800; font-size: 22px; + background: var(--imdb-yellow); + color: #000; + padding: 4px 10px; + border-radius: 4px; + text-decoration: none; +} +.primary-nav .menu { position: relative; cursor: pointer; padding: 6px 8px; color: #fff; } +.primary-nav .dropdown { + display: none; + position: absolute; top: 100%; left: 0; + background: #fff; color: var(--imdb-fg); + border: 1px solid var(--imdb-border); + padding: 8px 0; min-width: 220px; z-index: 50; +} +.primary-nav .menu:hover .dropdown, +.primary-nav .menu:focus-within .dropdown { display: block; } +.primary-nav .dropdown a { display: block; padding: 6px 14px; text-decoration: none; color: var(--imdb-fg); } +.primary-nav .dropdown a:hover { background: var(--imdb-panel); } + +.search { display: flex; flex: 1; max-width: 600px; gap: 0; } +.search select, .search input, .search button { border: 0; padding: 8px 10px; font-size: 14px; } +.search select { background: var(--imdb-panel); border-radius: 4px 0 0 4px; } +.search input { flex: 1; } +.search button { + background: var(--imdb-yellow); color: #000; font-weight: 700; + cursor: pointer; border-radius: 0 4px 4px 0; +} + +.user-area { display: flex; gap: 14px; } +.user-area a { color: #fff; text-decoration: none; font-weight: 500; } +.user-area a:hover { color: var(--imdb-yellow); } + +/* main */ +main { padding: 22px 0 60px; } +a { color: var(--imdb-link); text-decoration: none; } +a:hover { text-decoration: underline; } +.muted { color: var(--imdb-fg-muted); } +.crumb { display: inline-block; margin-bottom: 8px; } + +h1 { font-size: 28px; margin: 0 0 8px; } +h2 { font-size: 20px; margin: 28px 0 12px; padding-bottom: 4px; border-bottom: 4px solid var(--imdb-yellow); display: inline-block; } +h3 { font-size: 16px; margin: 16px 0 6px; } + +/* flash */ +.flashes { margin-bottom: 12px; } +.flash { padding: 8px 12px; border-radius: 4px; margin-bottom: 6px; font-size: 14px; } +.flash-success { background: #d8f5d0; color: #1a5b1a; } +.flash-error { background: #fde0e0; color: #8b1c1c; } +.flash-info { background: #d8eaf5; color: #1a4767; } + +/* hero / rows */ +.hero { padding: 14px 0 6px; } +.hero h1 { font-size: 32px; } +.hero .lede { color: var(--imdb-fg-muted); font-size: 16px; } + +.row { margin-top: 24px; } +.row-head { display: flex; align-items: center; justify-content: space-between; } +.row-head h2 { border-bottom: 4px solid var(--imdb-yellow); } +.see-all { font-size: 14px; } +.card-grid { + list-style: none; padding: 0; margin: 12px 0 0; + display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 14px; +} +.card { + background: var(--imdb-panel); border-radius: 6px; padding: 12px; + position: relative; display: flex; flex-direction: column; +} +.card .rank { font-size: 12px; color: var(--imdb-fg-muted); margin-bottom: 4px; } +.card .poster { width: 100%; aspect-ratio: 2 / 3; object-fit: cover; border-radius: 4px; margin-bottom: 8px; } +.card-title { font-weight: 600; color: var(--imdb-fg); display: block; margin-bottom: 4px; } +.card-meta { color: var(--imdb-fg-muted); font-size: 13px; } +.star { color: var(--imdb-yellow); } +.star.big { font-size: 28px; vertical-align: middle; } +.big { font-size: 26px; font-weight: 700; } +.small { color: var(--imdb-fg-muted); } + +/* title detail */ +.title-detail .title-head { + display: flex; justify-content: space-between; align-items: flex-end; + gap: 16px; margin-bottom: 8px; +} +.title-sub { color: var(--imdb-fg-muted); font-size: 14px; margin-top: 2px; } +.title-poster { float: right; width: 220px; aspect-ratio: 2 / 3; object-fit: cover; border-radius: 6px; margin: 0 0 12px 16px; } +.genre-pills { margin-top: 8px; } +.pill { + display: inline-block; border: 1px solid var(--imdb-border); + border-radius: 999px; padding: 3px 10px; font-size: 12px; + margin: 2px 4px 2px 0; +} +.tagline { font-style: italic; color: var(--imdb-fg-muted); margin: 6px 0 16px; } +.rating-box { text-align: center; } +.rating-label { font-size: 11px; color: var(--imdb-fg-muted); text-transform: uppercase; } +.rating-value { font-weight: 700; } +.rating-votes { font-size: 12px; color: var(--imdb-fg-muted); } +.rank-badge, .popularity-badge { + display: inline-block; background: var(--imdb-yellow); + color: #000; padding: 3px 8px; border-radius: 4px; + font-size: 12px; margin-top: 6px; margin-left: 4px; +} + +.plot, .cast, .boxoffice, .reviews, .user-actions { margin-top: 22px; } +.plot-short { font-weight: 600; } +.plot-full { color: #333; } + +.cast-grid { + list-style: none; padding: 0; + display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 10px; +} +.cast-card { background: var(--imdb-panel); padding: 8px 10px; border-radius: 4px; } +.cast-card .character { font-size: 12px; color: var(--imdb-fg-muted); } +.cast-photo { width: 60px; height: 80px; object-fit: cover; border-radius: 4px; margin-right: 8px; float: left; } + +.metadata { display: grid; grid-template-columns: 220px 1fr; gap: 6px 16px; } +.metadata dt { font-weight: 600; } +.metadata dd { margin: 0; } + +.user-actions form, .user-actions a.btn { display: inline-block; margin-right: 8px; } +.rate-form select { margin-left: 4px; } +.btn { + background: var(--imdb-yellow); + color: #000; font-weight: 600; + border: 0; padding: 7px 14px; border-radius: 4px; + cursor: pointer; text-decoration: none; + display: inline-block; +} +.btn:hover { background: var(--imdb-yellow-dark); text-decoration: none; } +.link { background: transparent; border: 0; color: var(--imdb-link); cursor: pointer; padding: 0; font-size: 13px; } +.inline { display: inline; margin-left: 8px; } + +.review-card { + background: var(--imdb-panel); padding: 12px 14px; + border-left: 4px solid var(--imdb-yellow); + margin-bottom: 12px; +} +.review-card header { display: flex; justify-content: space-between; align-items: baseline; } +.review-rating { font-weight: 600; } +.review-foot { color: var(--imdb-fg-muted); font-size: 12px; margin-top: 6px; } +.review-list { list-style: none; padding: 0; } + +/* tables */ +.chart-table, .credits-table { + width: 100%; border-collapse: collapse; margin-top: 8px; +} +.chart-table th, .chart-table td, .credits-table td { + text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--imdb-border); + vertical-align: top; +} +.chart-table th { background: var(--imdb-panel); font-weight: 600; } +.credits-table .year { width: 60px; color: var(--imdb-fg-muted); } +.credits-table .char { color: var(--imdb-fg-muted); } + +/* search + filters */ +.filters { + background: var(--imdb-panel); padding: 14px; + border-radius: 6px; display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; + margin-bottom: 16px; +} +.filters fieldset { + grid-column: 1 / -1; + border: 1px solid var(--imdb-border); padding: 10px; +} +.filters .check { display: inline-block; margin-right: 12px; font-weight: 400; } +.filters label, .form label { display: block; font-weight: 600; font-size: 13px; } +.filters input, .filters select, .form input, .form textarea, .form select { + width: 100%; padding: 6px 8px; font-size: 14px; + border: 1px solid var(--imdb-border); border-radius: 4px; + background: #fff; +} +.form { max-width: 480px; } +.form label { margin-bottom: 10px; } + +/* sort bar */ +.sort-bar { margin: 8px 0 12px; color: var(--imdb-fg-muted); } +.sort-bar a { margin-right: 4px; } +.sort-bar .active { font-weight: 700; color: var(--imdb-fg); } + +.result-list { list-style: none; padding: 0; } +.result-list li { padding: 10px 0; border-bottom: 1px solid var(--imdb-border); } + +/* person */ +.person-head { display: flex; gap: 16px; align-items: flex-start; } +.person-head-left { flex: 1; } +.person-head .person-sub { color: var(--imdb-fg-muted); } +.person-head .person-life { color: var(--imdb-fg-muted); font-size: 13px; } +.person-photo { width: 180px; aspect-ratio: 2 / 3; object-fit: cover; border-radius: 6px; } + +/* news */ +.news-list { list-style: none; padding: 0; } +.news-list li { padding: 12px 0; border-bottom: 1px solid var(--imdb-border); } +.news-meta { color: var(--imdb-fg-muted); font-size: 12px; } + +.account-stats { list-style: none; padding: 0; } +.account-stats li { padding: 6px 0; } + +.footer { + margin-top: 60px; + background: var(--imdb-top-bg); color: #ccc; + padding: 18px 0; font-size: 12px; +} +.footer a { color: var(--imdb-yellow); } + +/* IMDb navigation and shared layout. Keep all media local to the catalog. */ +body { font-family: Roboto, Helvetica, Arial, sans-serif; line-height: 1.5; } +.container { max-width: 1280px; padding-left: 24px; padding-right: 24px; } +main.container { padding-top: 32px; padding-bottom: 60px; } +.topbar { padding: 0; } +.topbar .nav { min-height: 56px; gap: 16px; padding-left: 12px; padding-right: 12px; } +.brand { flex: 0 0 64px; height: 32px; padding: 0 4px; text-align: center; font-family: Impact, 'Arial Narrow', sans-serif; font-size: 25px; line-height: 32px; letter-spacing: -1px; } +.primary-nav { flex: none; } +.primary-nav .menu { padding: 0; } +.menu summary { list-style: none; padding: 8px; border-radius: 4px; font-size: 14px; font-weight: 600; white-space: nowrap; cursor: pointer; } +.menu summary::-webkit-details-marker { display: none; } +.menu summary span { margin-right: 4px; font-size: 18px; } +.menu summary:hover, .menu[open] summary { background: #333; } +.primary-nav .dropdown { display: none; background: #1f1f1f; border-color: #444; box-shadow: 0 8px 20px #0006; border-radius: 4px; } +.primary-nav .menu:hover .dropdown, .primary-nav .menu:focus-within .dropdown { display: none; } +.primary-nav .menu[open] .dropdown { display: block; } +.primary-nav .dropdown a { color: #fff; padding: 10px 16px; } +.primary-nav .dropdown a:hover { background: #333; } +.search { max-width: none; min-width: 0; height: 32px; } +.search select { max-width: 90px; border-right: 1px solid #bbb; background: white; padding: 4px 8px; } +.search input { min-width: 0; padding: 4px 8px; background: white; } +.search button { padding: 3px 10px; background: white; color: #666; } +.search button svg { display: block; } +.user-area { align-items: center; gap: 18px; flex: none; font-size: 14px; white-space: nowrap; } +.user-area .nav-watchlist { border-left: 2px solid #444; padding-left: 18px; } +a:focus-visible, button:focus-visible, summary:focus-visible { outline: 2px solid #5799ef; outline-offset: 3px; } +h1 { font-size: 36px; font-weight: 400; line-height: 1.15; } +h2 { font-size: 22px; line-height: 1.2; font-weight: 600; border-bottom: 0; border-left: 4px solid var(--imdb-yellow); padding: 0 0 0 10px; } +.row-head h2 { border-bottom: 0; } +.row-head { gap: 16px; } +.eyebrow { font-size: 12px; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; } +.card-grid { gap: 16px; } +.card { padding: 0 0 16px; overflow: hidden; border-radius: 8px; } +.card .poster { display: block; border-radius: 0; margin-bottom: 12px; } +.card-title, .card-meta { padding: 0 12px; } +.card-title { font-size: 16px; font-weight: 400; } +.card-meta { font-size: 14px; } + +/* The offline home preserves the available catalog; it does not invent trailers. */ +.home-page { --imdb-bg: #000; --imdb-fg: #fff; --imdb-fg-muted: #aaa; --imdb-panel: #1a1a1a; --imdb-border: #333; --imdb-link: #5799ef; } +.home-page .hero { padding-top: 12px; } +.home-page .hero h1 { font-size: 28px; font-weight: 500; color: var(--imdb-yellow); } +.home-page .hero .lede { margin-top: 8px; } +.home-page .card-grid { display: grid; grid-auto-flow: column; grid-auto-columns: calc((100% - 80px) / 6); grid-template-columns: none; overflow-x: auto; padding-bottom: 12px; scroll-snap-type: x proximity; } +.home-page .card { scroll-snap-align: start; } +.home-page .row { margin-top: 32px; } +.home-page .row-head h2 { margin-top: 12px; } +.watchlist-prompt { margin-top: 36px; } +.watchlist-prompt p { text-align: center; margin: 32px 0 18px; } +.watchlist-prompt .btn { display: block; width: max-content; margin: 0 auto 48px; border-radius: 24px; padding: 8px 30px; background: #1a1a1a; color: #5799ef; } + +/* Poster list mirrors the chart's visual hierarchy without exposing detail-only task facts. */ +.chart-head { padding-top: 16px; } +.chart-head .muted { margin-top: 8px; } +.chart-layout { display: grid; grid-template-columns: minmax(0, 2.15fr) minmax(230px, 1fr); gap: 48px; } +.chart-count { margin: 22px 0; } +.chart-list { list-style: none; padding: 0; margin: 0; } +.chart-item { min-height: 118px; display: flex; align-items: center; gap: 12px; padding: 8px; border-radius: 12px; } +.chart-item:nth-child(odd) { background: #f8f8f8; } +.chart-poster-link { flex: 0 0 72px; } +.chart-poster { display: block; width: 72px; height: 106px; object-fit: cover; border-radius: 8px; } +.chart-item-content { flex: 1; min-width: 0; } +.chart-rank { display: inline-block; background: #3864b7; border-radius: 3px; padding: 0 10px; color: white; font-size: 14px; font-weight: 700; } +.chart-title { display: block; border: 0; padding: 0; margin: 4px 0; font-size: 16px; line-height: 1.3; } +.chart-title a { color: #222; } +.chart-meta, .chart-rating, .chart-gross { font-size: 14px; } +.chart-meta { color: #666; } +.chart-watchlist { flex: none; } +.chart-explore h2 { display: block; margin-top: 24px; } +.chart-explore h3 { font-size: 22px; margin-top: 36px; } +.chart-explore > a { display: flex; justify-content: space-between; gap: 8px; padding: 24px 16px; margin-top: 8px; border: 1px solid #ddd; border-radius: 12px; color: #222; } + +/* Title overview uses the existing poster and synopsis, with the source's dark header. */ +.title-overview { background: #1f1f1f; color: #fff; margin: -32px -24px 24px; padding: 16px 24px 28px; } +.title-jumps { display: flex; justify-content: flex-end; gap: 16px; font-size: 14px; margin-bottom: 16px; } +.title-jumps a { color: #fff; } +.title-detail .title-head { align-items: flex-start; margin-bottom: 20px; } +.title-head h1 { font-size: 48px; font-weight: 300; } +.title-overview .title-sub, .title-overview .rating-label, .title-overview .rating-votes, .title-overview .small, .title-overview .tagline { color: #bbb; } +.title-overview .pill { color: #fff; border-color: #777; font-size: 14px; padding: 3px 12px; } +.title-overview .rating-label { font-size: 12px; letter-spacing: 1.5px; } +.title-overview-body { display: grid; grid-template-columns: minmax(160px, 280px) minmax(0, 1fr); gap: 24px; } +.title-poster { float: none; width: 100%; max-width: 280px; border-radius: 8px; margin: 0; } +.title-synopsis { max-width: 700px; } +.title-synopsis .plot { margin-top: 0; } +.title-synopsis .plot-full { color: #fff; } +.title-synopsis .plot h2 { margin-top: 16px; } +.title-overview .rank-badge, .title-overview .popularity-badge { background: transparent; color: #ccc; padding: 0; } +.cast-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; } +.cast-card { display: flow-root; padding: 8px 0; background: transparent; min-height: 84px; } +.cast-photo { width: 80px; height: 80px; border-radius: 50%; margin-right: 16px; } +.cast-card .character { font-size: 14px; margin-top: 4px; } +.cast-card a { font-weight: 600; color: #222; } +.metadata { grid-template-columns: minmax(170px, 32%) 1fr; } +.metadata dt, .metadata dd { border-bottom: 1px solid #eee; padding: 8px 0; } +.user-actions { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; } +.user-actions form { margin: 0; } +.filters input[type=checkbox], .filters input[type=radio] { width: auto; } +.filters fieldset { min-width: 0; } +.review-card { border-left: 0; border: 1px solid #ddd; border-radius: 12px; background: #fff; padding: 20px; } +.footer { clear: both; text-align: center; background: #000; color: #aaa; padding: 40px 0; } +.footer-links { display: flex; flex-wrap: wrap; justify-content: center; gap: 16px 28px; margin-bottom: 28px; font-size: 14px; } +.footer-links a { color: #fff; } + +@media (max-width: 900px) { + .topbar .nav { flex-wrap: wrap; gap: 10px; padding-top: 12px; padding-bottom: 12px; } + .primary-nav { order: -1; } + .user-area { margin-left: auto; } + .search { order: 3; flex-basis: 100%; } + .chart-layout { grid-template-columns: minmax(0, 1fr); } + .chart-explore { border-top: 1px solid #ddd; } + .home-page .card-grid { grid-auto-columns: calc((100% - 48px) / 4); } + .title-head h1 { font-size: 36px; } + .title-overview-body { grid-template-columns: 200px minmax(0, 1fr); } +} +@media (max-width: 600px) { + .container { padding-left: 16px; padding-right: 16px; } + main.container { padding-top: 24px; } + .topbar .nav { gap: 6px; } + .menu summary { font-size: 0; padding-left: 4px; } + .menu summary span { font-size: 21px; margin: 0; } + .user-area { gap: 12px; font-size: 13px; } + .user-area .nav-watchlist { display: none; } + h1 { font-size: 30px; } + .row-head { align-items: baseline; } + .row-head h2 { font-size: 20px; } + .see-all { font-size: 12px; } + .home-page .card-grid { grid-auto-columns: 155px; } + .card-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .chart-head { padding-top: 4px; } + .chart-item { gap: 10px; flex-wrap: wrap; } + .chart-watchlist { margin-left: 82px; } + .title-overview { margin: -24px -16px 24px; padding: 16px; } + .title-jumps { justify-content: flex-start; gap: 12px; flex-wrap: wrap; font-size: 12px; } + .title-detail .title-head { display: block; } + .title-head h1 { font-size: 32px; } + .title-head-right { margin-top: 16px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } + .title-head-right .rating-box { text-align: left; } + .title-overview-body { grid-template-columns: 112px minmax(0, 1fr); gap: 16px; } + .title-synopsis { font-size: 14px; } + .title-synopsis .plot h2 { font-size: 18px; margin-top: 0; } + .tagline { display: none; } + .cast-grid { grid-template-columns: 1fr; gap: 12px; } + .metadata { grid-template-columns: minmax(110px, 44%) 1fr; gap: 0 12px; font-size: 14px; } + .metadata dd { overflow-wrap: anywhere; } + .filters { grid-template-columns: minmax(0, 1fr); } + .review-card header { gap: 12px; } + .review-rating { flex: none; } + .chart-table, .credits-table { font-size: 13px; } + .chart-table th, .chart-table td, .credits-table td { padding: 8px 4px; overflow-wrap: anywhere; } + .person-photo { width: 100px; } + .form { width: 100%; } +} + +.logout-form { margin: 0; } +.logout-form button { font: inherit; border: 0; padding: 0; background: transparent; color: #fff; cursor: pointer; } +.logout-form button:hover { color: var(--imdb-yellow); } diff --git a/sites/imdb/static/icons/.gitkeep b/sites/imdb/static/icons/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/imdb/static/icons/imdb.svg b/sites/imdb/static/icons/imdb.svg new file mode 100644 index 00000000..d4958001 --- /dev/null +++ b/sites/imdb/static/icons/imdb.svg @@ -0,0 +1 @@ + diff --git a/sites/imdb/static/js/.gitkeep b/sites/imdb/static/js/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/imdb/static/js/home.js b/sites/imdb/static/js/home.js new file mode 100644 index 00000000..63431bd3 --- /dev/null +++ b/sites/imdb/static/js/home.js @@ -0,0 +1,58 @@ +document.querySelectorAll('[data-open-dialog]').forEach(button => { + button.addEventListener('click', () => document.getElementById(button.dataset.openDialog).showModal()); +}); +const drawer = document.getElementById('nav-drawer'); +drawer?.querySelectorAll('a').forEach(link => link.addEventListener('click', () => drawer.close())); +drawer?.addEventListener('click', event => { + if (event.target === drawer) drawer.close(); +}); +const searchToggle = document.querySelector('.mobile-search'); +searchToggle?.addEventListener('click', () => { + const expanded = searchToggle.getAttribute('aria-expanded') !== 'true'; + searchToggle.setAttribute('aria-expanded', String(expanded)); + document.querySelector('.topbar').classList.toggle('search-open', expanded); + if (expanded) document.querySelector('#site-search input').focus(); +}); +document.querySelector('#site-search')?.addEventListener('keydown', event => { + if (event.key === 'Escape') { + document.querySelector('.topbar').classList.remove('search-open'); + searchToggle.setAttribute('aria-expanded', 'false'); + searchToggle.focus(); + } +}); +document.querySelectorAll('[data-scroll-rail]').forEach(button => { + button.addEventListener('click', () => { + const rail = button.parentElement.querySelector('[data-rail]'); + const atEnd = rail.scrollLeft + rail.clientWidth >= rail.scrollWidth - 2; + rail.scrollTo({left: atEnd ? 0 : rail.scrollLeft + rail.clientWidth * 0.85, + behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'instant' : 'smooth'}); + }); +}); +const slides = [...document.querySelectorAll('[data-slide]')]; +document.querySelectorAll('[data-episode-spotlight]').forEach(spotlight => { + spotlight.querySelectorAll('[data-episode-select]').forEach(button => { + button.addEventListener('click', () => { + spotlight.querySelectorAll('[data-episode-select]').forEach(choice => { + choice.setAttribute('aria-pressed', String(choice === button)); + }); + spotlight.querySelectorAll('[data-episode-panel]').forEach(panel => { + panel.hidden = panel.dataset.episodePanel !== button.dataset.episodeSelect; + }); + }); + }); +}); +let currentSlide = 0; +document.querySelectorAll('[data-hero-step]').forEach(button => { + button.addEventListener('click', () => { + if (!slides.length) return; + slides[currentSlide].hidden = true; + currentSlide = (currentSlide + Number(button.dataset.heroStep) + slides.length) % slides.length; + slides[currentSlide].hidden = false; + document.querySelectorAll('[data-up-next]').forEach(item => { + const distance = (Number(item.dataset.upNext) - currentSlide + slides.length) % slides.length; + item.hidden = distance < 1 || distance > 3; + item.style.order = distance; + }); + document.getElementById('hero-status').textContent = (currentSlide + 1) + ' of ' + slides.length; + }); +}); diff --git a/sites/imdb/tasks.jsonl b/sites/imdb/tasks.jsonl new file mode 100644 index 00000000..942e1134 --- /dev/null +++ b/sites/imdb/tasks.jsonl @@ -0,0 +1,20 @@ +{"web_name": "IMDb", "id": "IMDb--0", "ques": "Open the IMDb Top 250 Movies chart and identify the films ranked #1 and #3. For each film, report its title, runtime in minutes, and MPAA classification from its title information. Which of the two has the longer runtime?", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_0.py", "judge_rubric": "Confirm both movie-chart ranks; read and correctly bind each runtime and MPAA classification to its movie; identify the longer runtime. Either order of visiting the two titles is valid. Relevant navigation must belong to this run's local mirror; the exact homepage route or search wording is not an extra requirement. Equivalent wording and monetary units at the displayed precision are valid. This is a read-only task: business data must be unchanged."} +{"web_name": "IMDb", "id": "IMDb--2", "ques": "Open Domestic box office on the mirror. Identify the first-ranked movie by cumulative US & Canada gross among the titles in this catalog, and report its title and the production budget displayed on its title page.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_2.py", "judge_rubric": "Use the catalog cumulative-domestic-gross chart, identify its first movie, and read that same movie's production budget from title details. This is not the live IMDb weekend chart. Relevant navigation must belong to this run's local mirror; the exact homepage route or search wording is not an extra requirement. Equivalent wording and monetary units at the displayed precision are valid. This is a read-only task: business data must be unchanged."} +{"web_name": "IMDb", "id": "IMDb--7", "ques": "Open Christopher Nolan's profile. Among the movies shown in the Director section of this mirror's filmography, identify the highest IMDb rating and report the corresponding movie title, release year, and rating. If movies tie for the highest rating, report all of them.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_7.py", "judge_rubric": "Use the Director filmography on the correct person profile as the candidate set, considering only movies in this catalog. Compare their ratings and report all highest-rating ties with title, year and rating. Ratings may be obtained from any relevant on-site listing or details. Relevant navigation must belong to this run's local mirror; the exact homepage route or search wording is not an extra requirement. Equivalent wording and monetary units at the displayed precision are valid. This is a read-only task: business data must be unchanged."} +{"web_name": "IMDb", "id": "IMDb--9", "ques": "Use Advanced title search with Type set to Movie, Genre set to Drama, a minimum IMDb rating of 8.5, and Sort by set to IMDb rating. Report three highest-rated matching titles in descending rating order. Order within a tie does not matter; if several titles tie at the third-place cutoff, any three-title set containing all higher-rated titles is acceptable.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_9.py", "judge_rubric": "Submit the specified movie, Drama, minimum-rating and rating-order filters in Advanced title search. Report exactly three qualifying titles, retaining all titles above the third-place rating cutoff; either order or selection within the cutoff tie is valid. Results-page answers are valid. Relevant navigation must belong to this run's local mirror; the exact homepage route or search wording is not an extra requirement. Equivalent wording and monetary units at the displayed precision are valid. This is a read-only task: business data must be unchanged."} +{"web_name": "IMDb", "id": "IMDb--10", "ques": "Use Advanced title search for Movies in the Crime genre released from 1990 through 1999, with a minimum IMDb rating of 8.5, sorted by IMDb rating. For the highest-rated result, report the title, director or directors, and displayed worldwide gross. If results tie for the highest rating, report those details for every tied movie.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_10.py", "judge_rubric": "Submit all specified Advanced title search constraints with inclusive year bounds. Read director and worldwide gross for every highest-rating tie in those results, with values bound to the correct titles. Relevant navigation must belong to this run's local mirror; the exact homepage route or search wording is not an extra requirement. Equivalent wording and monetary units at the displayed precision are valid. This is a read-only task: business data must be unchanged."} +{"web_name": "IMDb", "id": "IMDb--12", "ques": "Open the Crime genre page. Identify the highest-rated movie and the highest-rated TV series among its titles. Report each title, start year, and IMDb rating, and say which group has the higher top rating or whether they are tied. Report all titles tied for the highest rating within either group; you may use other on-site pages to check title type and details.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_12.py", "judge_rubric": "Visit Crime genre, distinguish movie and TV-series membership, and compare the two highest-rating groups. Report all within-group ties, including title, start year and rating. On-site type checks through search or title pages are valid. Relevant navigation must belong to this run's local mirror; the exact homepage route or search wording is not an extra requirement. Equivalent wording and monetary units at the displayed precision are valid. This is a read-only task: business data must be unchanged."} +{"web_name": "IMDb", "id": "IMDb--14", "ques": "Compare The Dark Knight (2008) and Inception (2010) on IMDb. Report each film's displayed IMDb rating and worldwide gross, then identify which film has the higher rating and which has the larger worldwide gross, stating a tie if applicable.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_14.py", "judge_rubric": "Read both named films, bind each displayed rating and worldwide amount correctly, and identify the higher rating and larger worldwide gross. Either navigation order is valid. Relevant navigation must belong to this run's local mirror; the exact homepage route or search wording is not an extra requirement. Equivalent wording and monetary units at the displayed precision are valid. This is a read-only task: business data must be unchanged."} +{"web_name": "IMDb", "id": "IMDb--15", "ques": "Sign in as bob.c@test.com (password TestPass123!). From Bob's Watchlist, consider only TV series tagged Sci-Fi or Fantasy (either genre qualifies). Remove the qualifying series with the earliest start year; break any year tie by title in alphabetical order. Remove only that series. Report its title, start year, qualifying genre or genres, and confirm that it is no longer in Bob's Watchlist.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_15.py", "judge_rubric": "Select from Bob's initial Watchlist using the specified type, either genre, earliest-year and title tie-break rules. Remove exactly that item and no other business state; both watchlist and title-page removal controls are valid. Confirm its absence afterwards and report title, year and qualifying genres. Relevant navigation must belong to this run's local mirror; the exact homepage route or search wording is not an extra requirement. Equivalent wording and monetary units at the displayed precision are valid. "} +{"web_name": "IMDb", "id": "IMDb--16", "ques": "Sign in as carol.d@test.com (password TestPass123!). Among the Crime movies in Carol's Watchlist whose current personal rating is not 8/10 (unrated movies qualify), select the most recently released movie; break any year tie by title in alphabetical order. Set Carol's personal rating for only that movie to 8/10. Report its title and release year, and confirm the 8/10 rating on My ratings.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_16.py", "judge_rubric": "Select from Carol's initial Crime-movie Watchlist using her initial personal ratings, latest year and title tie-break. Insert or update exactly her selected personal rating to 8; do not change other ratings, reviews, watchlist entries or global title ratings. Confirm it on My ratings and report title and year. Relevant navigation must belong to this run's local mirror; the exact homepage route or search wording is not an extra requirement. Equivalent wording and monetary units at the displayed precision are valid. "} +{"web_name": "IMDb", "id": "IMDb--17", "ques": "Sign in to IMDb as alice.j@test.com (password TestPass123!). Open the page for Interstellar (2014) and submit a written review with the headline 'Brilliant sci-fi epic' along with a 1-10 rating of 10. Confirm afterwards that the new review appears on Interstellar's user reviews page.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_17.py", "judge_rubric": "Create exactly one new review by the specified synthetic account for the specified movie with the requested exact headline, review rating 10 and any nonempty review body. Verify it appears on that movie's reviews page. Any review-list sort is valid; a separate personal-rating write is not required. Other business state must remain unchanged. Relevant navigation must belong to this run's local mirror; the exact homepage route or search wording is not an extra requirement. Equivalent wording and monetary units at the displayed precision are valid. "} +{"web_name": "IMDb", "id": "IMDb--18", "ques": "Plan a two-film animation double feature using this mirror’s catalog. Choose two different movies released in 2010 or earlier; both must have an MPAA rating of G or PG, and their combined runtime must be no more than 190 minutes. Maximize the sum of the two displayed IMDb ratings. Report every tied best pair, giving each film’s title, displayed IMDb rating, runtime and MPAA rating, plus each pair’s combined runtime and summed rating. Use the values displayed on the mirror.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_18.py", "judge_rubric": "Find the complete optimal pair set among the mirror’s Animation movies subject to the stated year, MPAA and combined-runtime constraints. Bind the displayed rating, runtime and classification to each selected film, and give correct totals. Include all tied optima and no non-optimal pair. Do not treat Not Rated or a missing classification as G/PG. The order of films within a pair is immaterial. Relevant source facts must be observed on this run's local mirror; equivalent routes and either order of independent lookups are valid. This is a read-only task: business data must remain unchanged."} +{"web_name": "IMDb", "id": "IMDb--19", "ques": "Using this mirror’s catalog, identify every movie listed both in Steven Spielberg’s Director filmography and Tom Hanks’s Actor filmography. For each shared movie, report its title, displayed Budget and Opening weekend US & Canada. Calculate Opening weekend US & Canada ÷ Budget × 100 using those displayed amounts, and round the percentage to one decimal place. Which shared movie has the largest percentage? Include all ties.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_19.py", "judge_rubric": "Find the complete shared-movie set for the specified people and credit roles. For every shared film bind the correct displayed Budget and Opening weekend US & Canada values, calculate the specified percentage to one decimal place from displayed amounts, and identify the entire maximum set. A worldwide or domestic lifetime total is not an opening weekend; a Writer or other credit is not an Actor/Director credit. Accept equivalent currency units and formatting. Relevant source facts must be observed on this run's local mirror; equivalent routes and either order of independent lookups are valid. This is a read-only task: business data must remain unchanged."} +{"web_name": "IMDb", "id": "IMDb--20", "ques": "For every movie in Leonardo DiCaprio’s Actor filmography on this mirror, report a table containing the title, displayed Gross US & Canada and displayed Gross worldwide. For each movie calculate (Gross worldwide − Gross US & Canada) ÷ Gross worldwide × 100 using exactly the displayed amounts, and round to one decimal place. Identify every movie with the largest resulting percentage. This is a calculation from the two displayed totals, not a separate reported International gross statistic.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_20.py", "judge_rubric": "Cover every movie in the specified Actor filmography and bind each row’s two displayed gross amounts to the correct film. Calculate the explicitly defined remainder share from those displayed values and round to one decimal place. Identify the complete maximum set. Do not use Opening weekend or Budget, confuse currency magnitudes, import a separate International value, omit rows, or substitute real-world titles absent from this mirror. Relevant source facts must be observed on this run's local mirror; equivalent routes and either order of independent lookups are valid. This is a read-only task: business data must remain unchanged."} +{"web_name": "IMDb", "id": "IMDb--21", "ques": "For Alice (alice.j@test.com) and Bob (bob.c@test.com), inspect their own My ratings and Watchlist on this mirror; both accounts use password TestPass123!. For each account, list every movie that the account rated 9 or 10 but has not added to its own Watchlist. Report the account, film title, that account’s rating, director(s) and runtime. Keep all ratings, reviews and Watchlists unchanged.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_21.py", "judge_rubric": "Compute the per-account difference between movies personally rated at least nine and that same account’s Watchlist, with no cross-account mixing. Report every qualifying film and bind the correct account, personal rating, complete Director set and runtime. IMDb average rating is not personal rating; TV series do not qualify. Preserve all business state. Accept equivalent UI routes and order. Relevant source facts must be observed on this run's local mirror; equivalent routes and either order of independent lookups are valid. This is a read-only task: business data must remain unchanged."} +{"web_name": "IMDb", "id": "IMDb--22", "ques": "Sign in as Alice (alice.j@test.com; password TestPass123!). Form the union of the movies in her Watchlist and My ratings, counting a movie only once. Among reviews of those movies, find every group where the same movie has an identical review headline posted by different authors. For each group, report the movie title and year, the repeated headline, and every matching review’s author, review score, review date and helpful count. Do not change any reviews, ratings or Watchlists.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_22.py", "judge_rubric": "Use the deduplicated movie union from Alice’s two account lists. Find every within-movie identical-headline group with different authors, and report the complete group with correct title/year/headline and author-bound review score/date/helpful count. Multiple reviews with different headlines are not a duplicate group; identical text on different movies would not qualify. Do not confuse IMDb average or Alice’s personal film rating with a review’s score. Preserve business state; row order is immaterial. Relevant source facts must be observed on this run's local mirror; equivalent routes and either order of independent lookups are valid. This is a read-only task: business data must remain unchanged."} +{"web_name": "IMDb", "id": "IMDb--23", "ques": "Using every movie listed in Tom Hanks’s Actor filmography on this mirror, build a chronological table of the title and the full Release date displayed on each movie’s detail page. Identify every pair of consecutive movies in that timeline with the smallest gap in calendar days. Report both titles, both dates and the day gap for every tied pair. Use the displayed Release date, not just the year in the filmography, and do not add real-world filmography titles absent from this mirror.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_23.py", "judge_rubric": "Include every mirror movie in the specified Actor filmography, bind its exact displayed Release date to the correct title, and order the timeline chronologically. Compute ordinary elapsed calendar days between consecutive dates and report the entire tied minimum set with both dates. Do not substitute title year, globally earliest release, a different market, or unlisted films. Accept ISO or unambiguous written dates and equivalent timeline formats. Relevant source facts must be observed on this run's local mirror; equivalent routes and either order of independent lookups are valid. This is a read-only task: business data must remain unchanged."} +{"web_name": "IMDb", "id": "IMDb--24", "ques": "Sign in as david.k@test.com (password TestPass123!). Build a weeknight science-fiction Watchlist: use Advanced title search for Movies tagged Sci-Fi, released from 1960 through 1989, with a minimum IMDb rating of 8.0. Among those results, add every movie with a displayed runtime of 120 minutes or less that is not already in David's Watchlist. Keep all existing entries. Confirm the additions on David's Watchlist and report each added movie's title, year, and runtime.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_24.py", "judge_rubric": "Submit all specified Advanced title search constraints with inclusive year bounds. Use the displayed runtime and David's initial Watchlist membership to determine the complete addition set; results-page runtime evidence is valid. Add exactly the missing qualifying movies under David's account, preserve existing Watchlist rows and all other business state, and confirm the saved movies on his Watchlist. Report their correctly bound titles, years, and runtimes. Either addition order and any genuine title-bound Watchlist control are valid; no fixed search wording or unnecessary detail-page visit is required. Relevant pages and actions must belong to this run's local mirror."} +{"web_name": "IMDb", "id": "IMDb--25", "ques": "Use Bob's personal ratings (bob.c@test.com, password TestPass123!) to find the movies he has rated 10/10. Add those recommendations to David's Watchlist (david.k@test.com, password TestPass123!), adding only movies that are not already there. Do not copy TV series or change either person's ratings. Keep all existing Watchlist entries. Report the titles and years of the movies you added and any qualifying recommendation that was already saved, then confirm David's Watchlist.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_25.py", "judge_rubric": "Read Bob's personal ratings, distinguish Movie from TV-series membership on relevant on-site pages, and compare the qualifying movie set against David's initial Watchlist. Insert exactly the missing qualifying movies for David. Existing Watchlist rows for both users, all personal ratings, reviews, users, and catalog data must be unchanged. Report the added titles and years and identify the already-saved qualifying recommendation. Verify the destination Watchlist after the changes. Reading the two accounts in either order, using either normal Sign Out control, and saving from a correct title or chart control are valid; all ownership evidence and pages must be local."} +{"web_name": "IMDb", "id": "IMDb--26", "ques": "Create a separate IMDb account for an unwatched queue. First sign in as alice.j@test.com (password TestPass123!) and compare Alice's Watchlist with her My ratings list. The queue should contain exactly the movies in her Watchlist that have no personal rating. Register a new synthetic account named Weekend Viewer with email weekend.viewer@test.com and password TestPass123!, and put that queue in the new account's Watchlist. Keep Alice's account and saved data unchanged. Sign out of the new account and sign back in to confirm its Watchlist persists, then report the saved movie titles and years.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_26.py", "judge_rubric": "Derive the source set from Alice's initial Watchlist minus her personal-rating titles; the globally displayed IMDb rating and an unrated form's default selection do not count as a saved personal rating. Create exactly one new user with the requested name/email and working requested credentials, then add exactly the derived movies to that user's Watchlist. Preserve all original users and business rows; create no ratings or reviews. Observe registration, real sign-out, successful subsequent login to the new account, and its final saved-title list. Registration's automatic login and an earlier extra login are both valid. Report the correct titles and years; do not require a hidden ordering of additions."} +{"web_name": "IMDb", "id": "IMDb--27", "ques": "Sign in as alice.j@test.com (password TestPass123!). Reconcile Alice's saved movies with her reviews and My ratings: find the movie in her Watchlist that already has a scored review written by Alice but has no personal rating. Set its personal rating to the score in Alice's own review, without submitting or changing any review. Confirm the personal score on My ratings, and report the movie title and year, Alice's review headline, and the score you saved.", "web": "http://localhost:40024/", "upstream_url": "https://www.imdb.com/", "verifier_path": "sites/imdb/verify/verify_27.py", "judge_rubric": "Use Alice's initial Watchlist and personal-rating list to establish the missing personal rating, and use a visible review attributed to Alice as the score source. A review by another author, a global IMDb score, or a dropdown's default value is not equivalent. The frozen initial data must identify one qualifying movie and one scored Alice review for it. Insert exactly the missing Alice/title personal-rating row with that review's score; preserve every review, all original ratings and Watchlist rows, users, and catalog data. Confirm My ratings afterwards and bind title/year/headline/score correctly. A featured review or user-review list is valid if author and score are visible; any review-list sort is valid. All source/action/confirmation evidence must be local."} diff --git a/sites/imdb/templates/.gitkeep b/sites/imdb/templates/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/imdb/templates/404.html b/sites/imdb/templates/404.html new file mode 100644 index 00000000..20ae86e8 --- /dev/null +++ b/sites/imdb/templates/404.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% block title %}404 Not Found - IMDb{% endblock %} +{% block content %} +

404 — Not Found

+

We couldn't find that page in the IMDb mirror.

+

Back to homepage

+{% endblock %} diff --git a/sites/imdb/templates/account.html b/sites/imdb/templates/account.html new file mode 100644 index 00000000..d0e12130 --- /dev/null +++ b/sites/imdb/templates/account.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% block title %}{{ current_user.name }} - Account{% endblock %} +{% block content %} +

Your account

+

{{ current_user.name }} · {{ current_user.email }}

+ +
+ + +
+{% endblock %} diff --git a/sites/imdb/templates/advanced_search.html b/sites/imdb/templates/advanced_search.html new file mode 100644 index 00000000..a07c5e1e --- /dev/null +++ b/sites/imdb/templates/advanced_search.html @@ -0,0 +1,62 @@ +{% extends 'base.html' %} +{% block title %}Advanced title search - IMDb{% endblock %} +{% block content %} +

Advanced title search

+
+ + + + +
+ Genres (all selected must apply) + {% for g in all_genres %} + + {% endfor %} +
+ + +
+ +

{{ results|length }} result{{ 's' if results|length != 1 }}

+
    + {% for t in results %} +
  1. + {{ t.primary_title }} + + ({{ t.year }}) · + {{ '%.1f'|format(t.rating_avg) }} · + {{ t.num_votes|compact_votes }} votes · + {% if t.runtime_min %}{{ t.runtime_min }}m ·{% endif %} + {% for g in t.genres %}{{ g.name }}{{ ', ' if not loop.last }}{% endfor %} + +

    {{ t.plot_short }}

    +
  2. + {% else %} +
  3. No matches. Loosen the filters.
  4. + {% endfor %} +
+{% endblock %} diff --git a/sites/imdb/templates/base.html b/sites/imdb/templates/base.html new file mode 100644 index 00000000..344ad7c9 --- /dev/null +++ b/sites/imdb/templates/base.html @@ -0,0 +1,87 @@ + + + + + {% block title %}{{ site_name }}{% endblock %} + + + + + + + + +
+ +
+ + + +
+ {% with msgs = get_flashed_messages(with_categories=true) %} + {% if msgs %}
{% for cat, msg in msgs %}
{{ msg }}
{% endfor %}
{% endif %} + {% endwith %} + {% block content %}{% endblock %} +
+ + + diff --git a/sites/imdb/templates/chart.html b/sites/imdb/templates/chart.html new file mode 100644 index 00000000..ba89168c --- /dev/null +++ b/sites/imdb/templates/chart.html @@ -0,0 +1,42 @@ +{% extends 'base.html' %} +{% block title %}{{ chart_name }} - IMDb{% endblock %} +{% block content %} +
+

IMDb Charts

+

{{ chart_name }}

+

{{ description }}

+
+
+
+

{{ titles|length }} titles

+
    + {% for t in titles %} +
  1. + {% if t.poster_path %}{% endif %} +
    + #{% if chart_slug in ('top', 'toptv') %}{{ t.top_rank }}{% elif chart_slug == 'moviemeter' %}{{ t.popularity_rank }}{% else %}{{ loop.index }}{% endif %} +

    {{ t.primary_title }}

    +
    {{ t.year }}
    +
    {{ '%.1f'|format(t.rating_avg) }} ({{ t.num_votes|compact_votes }})
    + {% if chart_slug == 'boxoffice' %}
    Gross US & Canada: {{ t.box_office_us|money }}
    {% endif %} +
    + {% if current_user.is_authenticated %} +
    + + +
    + {% endif %} +
  2. + {% endfor %} +
+
+ +
+{% endblock %} diff --git a/sites/imdb/templates/genre_browse.html b/sites/imdb/templates/genre_browse.html new file mode 100644 index 00000000..4d6e8836 --- /dev/null +++ b/sites/imdb/templates/genre_browse.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% block title %}{{ genre.name }} movies and TV - IMDb{% endblock %} +{% block content %} +

Browse {{ genre.name }}

+

Top-rated {{ genre.name|lower }} titles in the mirror.

+
    + {% for t in titles %} +
  • + {{ t.primary_title }} + ({{ t.year }}) · + {{ '%.1f'|format(t.rating_avg) }} + +
  • + {% endfor %} +
+{% endblock %} diff --git a/sites/imdb/templates/home_collection.html b/sites/imdb/templates/home_collection.html new file mode 100644 index 00000000..9d95610d --- /dev/null +++ b/sites/imdb/templates/home_collection.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% from 'home_macros.html' import snapshot_card, birthday_row, ranked_person with context %} +{% block title %}{{ heading }} - IMDb{% endblock %} +{% block body_class %}home-page{% endblock %} +{% block content %} +
+ ‹ IMDb home +

{{ heading }}

{{ description }}

+ {% if items %} +

IMDb snapshot ¡ {{ items[0].captured_at[:10] }}

+ {% if collection == 'birthday' %}{{ birthday_row(items) }} + {% elif collection == 'starmeter' %}
{% for item in items %}{{ ranked_person(item) }}{% endfor %}
+ {% else %}
    {% for item in items %}{{ snapshot_card(item) }}{% endfor %}
{% endif %} + {% else %}

This collection has not been captured in this seed.

{% endif %} +
+{% endblock %} diff --git a/sites/imdb/templates/home_feature.html b/sites/imdb/templates/home_feature.html new file mode 100644 index 00000000..fdc29038 --- /dev/null +++ b/sites/imdb/templates/home_feature.html @@ -0,0 +1,74 @@ +{% extends 'base.html' %} +{% from 'home_macros.html' import snapshot_row, birthday_row, starmeter_row with context %} +{% block title %}{{ feature.heading }} - IMDb{% endblock %} +{% block body_class %}home-page{% endblock %} +{% block content %} +{% if feature.kind == 'editorial' and feature_items %} +
+
+ ‹ Featured today +
+

IMDb MOST
ANTICIPATED

+

{{ feature.payload.page_title }}

+

{{ feature.payload.page_subtitle }}

+ Hwang Jung-min and Hoyeon in Hopeu (2026) +

{{ feature_items|length }} titles ¡ movies and series arriving this month

+
+
+ {% for item in feature_items %} +
+ + {{ item.poster_alt }} + +
+

{{ item.title }}

+

+ {% if item.rating %}★ {{ item.rating }}{% if item.votes %} ({{ item.votes }}){% endif %}{% endif %} + {% if item.popularity %}{{ item.popularity }}{% endif %} + {% for value in item.metadata %}{{ value }}{% endfor %} +

+

{{ item.release_context }}

+

{{ item.description }}

+
+ Title details + {% if item.source_id in catalog_ids %}Catalog & Watchlist{% endif %} +
+
Featuring{% for name in item.featuring %}{{ name }}{% endfor %}
+
+
+ {% endfor %} +
+
Snapshot source

Observed {{ feature.payload.observed_at[:10] }} on IMDb. Ratings, popularity and availability are a dated source snapshot.

{{ feature.source_url }}

+ ‹ Back to IMDb home +
+
+{% else %} +‹ IMDb home +

{{ feature.heading }}

+ {% if feature.image_path %}{{ feature.heading }}{% endif %} + {% if feature.poster_path %}Poster{% endif %} + {% if feature.kind == 'hero' %}

{{ feature.payload.title }} ¡ {{ feature.payload.duration }}

This is the official trailer artwork. Video playback is not bundled with this offline mirror.

+ {% else %}

{{ feature.subtitle }}

{% endif %} + {% if feature.kind == 'news' %}

{{ feature.payload.source }} ¡ {{ feature.payload.date[:10] }}

{% endif %} + {% if feature.kind in ['birthday', 'streaming', 'tv_schedule', 'starmeter'] %} +

IMDb snapshot ¡ {{ feature.captured_at[:10] }}

+
+ {% if feature.kind == 'starmeter' %}
STARmeter rank at capture
{{ feature.payload.rank }}
{% if feature.payload.known_for %}
Known for
{{ feature.payload.known_for|join(' ¡ ') }}
{% endif %}{% endif %} + {% if feature.payload.rating %}
IMDb rating at capture
★ {{ feature.payload.rating }}/10
{% endif %} + {% if feature.payload.age %}
{{ 'Years' if '-' in feature.payload.age else 'Age at capture' }}
{{ feature.payload.age }}
{% endif %} + {% if feature.payload.episode %}
Episode / season
{{ feature.payload.episode }}
Air date shown by IMDb
{{ feature.payload.air_date }}
{% endif %} + {% if feature.payload.service %}
Streaming service at capture
{{ feature.payload.service }}
{% endif %} +
+ {% if feature.kind == 'streaming' %}

Watch options

Listed with {{ feature.payload.service }} in the captured IMDb homepage. Availability is region- and date-dependent. Streaming playback and subscriptions are not available in this offline mirror.

{% endif %} + {% if catalog_title %}

The benchmark catalog is an earlier, separately preserved snapshot.

{% endif %} + {% if catalog_person %}{% endif %} +

Explore this collection ›

+ {% endif %} + {% if feature.payload.episodes %}

Season {{ feature.payload.season }} episodes

{% for episode in feature.payload.episodes %}

E{{ episode.number }} ¡ {{ episode.title }}

{% if episode.image_path %}{{ episode.title }}{% endif %}

{{ episode.info }}

★ {{ episode.rating }}/10

{{ episode.plot }}

{% endfor %}{% endif %} + {% if feature.kind in ['editorial', 'topic', 'spotlight'] %}

This is a homepage feature preview. The full editorial page is not included in this snapshot.

{% endif %} +
Snapshot source

Captured {{ feature.captured_at[:10] }} from IMDb.

{{ feature.source_url }}

+ Browse the offline catalog › +
+{% if related and feature.kind in ['streaming', 'tv_schedule', 'birthday', 'starmeter'] %}

More to explore

{% if feature.kind == 'birthday' %}{{ birthday_row(related) }}{% elif feature.kind == 'starmeter' %}{{ starmeter_row(related) }}{% else %}{{ snapshot_row(related, 'titles') }}{% endif %}
{% endif %} +{% endif %} +{% endblock %} diff --git a/sites/imdb/templates/home_feature_title.html b/sites/imdb/templates/home_feature_title.html new file mode 100644 index 00000000..9f9de4ae --- /dev/null +++ b/sites/imdb/templates/home_feature_title.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} +{% block title %}{{ item.title }} — {{ feature.heading }} - IMDb{% endblock %} +{% block body_class %}home-page{% endblock %} +{% block content %} +
+ ‹ Back to all {{ feature.payload.item_count }} titles +
+ {{ item.poster_alt }} +
+

IMDb Most Anticipated ¡ September

+

{{ item.title }}

+

+ {% if item.rating %}★ {{ item.rating }}{% if item.votes %} ({{ item.votes }}){% endif %}{% endif %} + {% if item.popularity %}{{ item.popularity }}{% endif %} + {% for value in item.metadata %}{{ value }}{% endfor %} +

+

{{ item.release_context }}

+

{{ item.description }}

+ {% if catalog_title %}{% endif %} +
+
+
+

Featuring

+
{% for name in item.featuring %}{{ name }}{% endfor %}
+
+
Snapshot source

Observed {{ feature.payload.observed_at[:10] }} on IMDb.

{{ item.source_url }}

+
+{% endblock %} diff --git a/sites/imdb/templates/home_macros.html b/sites/imdb/templates/home_macros.html new file mode 100644 index 00000000..bac21b1c --- /dev/null +++ b/sites/imdb/templates/home_macros.html @@ -0,0 +1,52 @@ +{% macro title_card(t, ranked=false, rank=0) %} +
  • +
    {% if t.poster_path %}{{ t.primary_title }}{% else %}Poster unavailable{% endif %} + {% if current_user.is_authenticated %} +
    + {% else %}+{% endif %} +
    +
    +
    ★ {{ '%.1f'|format(t.rating_avg) }}☆
    + {% if ranked %}{{ rank }}. {% endif %}{{ t.primary_title }} + {% if current_user.is_authenticated %}
    {% else %}+ Watchlist{% endif %} + +
    +
  • +{% endmacro %} + +{% macro snapshot_card(feature) %} +
  • + {{ feature.heading }} +
    +
    {% if feature.payload.rating %}★ {{ feature.payload.rating }}{% else %}Not yet rated{% endif %}
    + {{ feature.heading }} + {% if feature.kind == 'tv_schedule' %}
    {{ feature.payload.episode }}{{ feature.payload.air_date }}
    {% endif %} + {{ 'Watch options' if feature.kind == 'streaming' else 'Episode details' }} +
    +
  • +{% endmacro %} + +{% macro snapshot_row(items, label) %} +
      {% for feature in items %}{{ snapshot_card(feature) }}{% endfor %}
    +{% endmacro %} + +{% macro birthday_row(items) %} + +{% endmacro %} + +{% macro ranked_person(feature) %} + + {% if feature.image_path %}{% else %}Photo unavailable{% endif %} + {{ feature.payload.rank }}

    {{ feature.heading }}

    +
    +{% endmacro %} + +{% macro starmeter_row(items) %} +
    {% for feature in items %}{{ ranked_person(feature) }}{% endfor %}
    +{% endmacro %} + +{% macro title_row(heading, titles, href, caption, ranked=false) %} +{% if heading %}

    {{ heading }} ›

    {% endif %} +{% if caption %}

    {{ caption }}

    {% endif %} +
      {% for t in titles %}{{ title_card(t, ranked, loop.index) }}{% endfor %}
    +{% endmacro %} diff --git a/sites/imdb/templates/index.html b/sites/imdb/templates/index.html new file mode 100644 index 00000000..7550a631 --- /dev/null +++ b/sites/imdb/templates/index.html @@ -0,0 +1,90 @@ +{% extends 'base.html' %} +{% from 'home_macros.html' import title_row, snapshot_row, birthday_row, starmeter_row with context %} +{% block body_class %}home-page{% endblock %} +{% block content %} +{% set heroes = features.get('hero', []) %} +{% if heroes %} +
    +
    + {% for feature in heroes %} + + {% endfor %} + + + 1 of {{ heroes|length }} +
    + +
    +{% endif %} + + +{% if features.get('editorial') %} + +{% endif %} +{% for feature in features.get('spotlight', []) %} +
    +

    {{ feature.heading }} ›

    {{ feature.subtitle }}

    +
    {{ feature.payload.title }}

    {{ feature.payload.title }}

    {% for credit in feature.payload.credits %}

    {{ credit.role }} {{ credit.names|join(' ¡ ') }}

    {% endfor %}All captured episodes ›
    +
    + {% if feature.payload.episodes %}

    Season {{ feature.payload.season }} ¡ Episode ratings

    {% for episode in feature.payload.episodes %}{% endfor %}
    + {% for episode in feature.payload.episodes %}
    + {% if episode.image_path %}{{ episode.title }}{% endif %} +

    {{ episode.info }}

    {{ episode.title }} ›

    ★ {{ episode.rating }}/10

    {% if episode.plot %}

    {{ episode.plot }}

    {% endif %}
    +
    {% endfor %} +
    {% endif %} +
    +{% endfor %} +{% if features.get('starmeter') %} +

    Trending people ›

    STARmeter ¡ IMDb snapshot {{ features.starmeter[0].captured_at[:10] }}

    {{ starmeter_row(features.starmeter[:12]) }}
    +{% else %}

    Explore people ›

    People from the local film and TV catalog

    +
    {% for person in people %}

    {{ person.name }}

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

    What to watch

    + {{ title_row('Top picks for you', top_picks, url_for('chart_top'), 'Top-rated titles from this catalog') }} +
    +

    From your Watchlist ›

    + {% if watchlist %}{{ title_row('', watchlist, '', '') }} + {% else %}

    {{ 'Your Watchlist is empty' if current_user.is_authenticated else 'Sign in to access your Watchlist' }}

    Save shows and movies to keep track of what you want to watch.

    {% endif %} +
    +
    {{ title_row('Top 10 on IMDb', trending, url_for('chart_moviemeter'), 'Most popular in the catalog snapshot', true) }}
    +
    {{ title_row('Fan favorites', fan_favorites, url_for('advanced_title_search', sort='votes'), 'Most-voted titles in the catalog snapshot') }}
    +

    Popular interests ›

    {% for genre in interests %}{{ genre.name }} ›{% endfor %}
    +

    Explore what’s streaming

    + {% if features.get('streaming') %}

    Included with Prime ¡ IMDb snapshot {{ features.streaming[0].captured_at[:10] }} ¡ playback not bundled

    {{ snapshot_row(features.streaming, 'streaming titles') }} + {% else %}

    Streaming availability has not been captured in this seed.

    {% endif %} +
    +

    Explore movies & TV shows

    + {{ title_row('Latest catalog releases', in_theaters, url_for('advanced_title_search'), 'Sorted by recorded release date; not live cinema listings') }} +
    +

    Domestic box office ›

    Cumulative US & Canada gross ¡ catalog snapshot

    +
      {% for t in box_office %}
    1. {{ loop.index }}
      {{ t.primary_title }}{{ t.box_office_us|money }}
    2. {% endfor %}
    +
    +{% if features.get('tv_schedule') %}

    Current & upcoming TV shows ›

    Episode & season dates ¡ IMDb snapshot {{ features.tv_schedule[0].captured_at[:10] }}

    {{ snapshot_row(features.tv_schedule, 'TV shows') }}
    {% endif %} +{% if heroes %} +

    Featured trailer previews ›

    {% for feature in heroes %}{% endfor %}
    +{% endif %} +

    More to explore

    Born today ›

    + {% if features.get('birthday') %}

    People born on {{ features.birthday[0].captured_at[5:10] }} ¡ ages at {{ features.birthday[0].captured_at[:10] }}

    {{ birthday_row(features.birthday) }}{% else %}

    Birthdays have not been captured in this seed.

    {% endif %} +
    +

    Latest IMDb news ›

    +
    {% if features.get('news') %}{% endif %} + +
    +
    +

    Recently viewed

    {% if recent_titles %}
    {% endif %}
    +{% if recent_titles %}
    {% for t in recent_titles %}{{ t.primary_title }}{% endfor %}
    {% else %}

    You have no recently viewed pages

    {% endif %}
    +{% endblock %} diff --git a/sites/imdb/templates/login.html b/sites/imdb/templates/login.html new file mode 100644 index 00000000..fdaca105 --- /dev/null +++ b/sites/imdb/templates/login.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} +{% block title %}Sign in - IMDb{% endblock %} +{% block content %} +

    Sign in

    +
    + + + + +
    +

    New here? Create an account.

    +

    Demo accounts: alice.j@test.com / bob.c@test.com / carol.d@test.com / david.k@test.com — password TestPass123!

    +{% endblock %} diff --git a/sites/imdb/templates/my_ratings.html b/sites/imdb/templates/my_ratings.html new file mode 100644 index 00000000..c1ebabad --- /dev/null +++ b/sites/imdb/templates/my_ratings.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} +{% block title %}Your ratings - IMDb{% endblock %} +{% block content %} +

    {{ current_user.name }}'s ratings

    + + + + {% for r, t in rows %} + + + + + + + {% else %} + + {% endfor %} + +
    TitleYearYour ratingIMDb rating
    {{ t.primary_title }}{{ t.year }} {{ r.rating }}/10 {{ '%.1f'|format(t.rating_avg) }}
    No ratings yet.
    +{% endblock %} diff --git a/sites/imdb/templates/name_detail.html b/sites/imdb/templates/name_detail.html new file mode 100644 index 00000000..7746c4c4 --- /dev/null +++ b/sites/imdb/templates/name_detail.html @@ -0,0 +1,63 @@ +{% extends 'base.html' %} +{% block title %}{{ person.name }} - IMDb{% endblock %} +{% block content %} +
    +
    +
    +

    {{ person.name }}

    +
    + {% if person.primary_profession %}{{ person.primary_profession.replace(',', ' · ')|safe }}{% endif %} +
    +
    + Born {{ person.birth_year or '—' }}{% if person.birth_place %}, {{ person.birth_place }}{% endif %} + {% if person.death_year %} · Died {{ person.death_year }}{% endif %} +
    +
    + {% if person.photo_path %} + + {% endif %} +
    + + {% if person.bio %} +
    +

    Mini bio

    +

    {{ person.bio }}

    +
    + {% endif %} + + {% if known_for %} +
    +

    Known for

    +
      + {% for t in known_for %} +
    • + {{ t.primary_title }} +
      {{ t.year }} · {{ '%.1f'|format(t.rating_avg) }}
      +
    • + {% endfor %} +
    +
    + {% endif %} + + {% set role_labels = {'actor':'Actor', 'director':'Director', 'writer':'Writer', + 'producer':'Producer', 'composer':'Composer'} %} + {% for role, items in grouped.items() %} + {% if items %} +
    +

    {{ role_labels[role] }} — {{ items|length }} credit{{ 's' if items|length != 1 }}

    + + + {% for c in items %} + + + + + + {% endfor %} + +
    {{ c.title.year or '' }}{{ c.title.primary_title }}{% if c.character %}as {{ c.character }}{% endif %}
    +
    + {% endif %} + {% endfor %} +
    +{% endblock %} diff --git a/sites/imdb/templates/news.html b/sites/imdb/templates/news.html new file mode 100644 index 00000000..26c3b48b --- /dev/null +++ b/sites/imdb/templates/news.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} +{% block title %}IMDb News{% endblock %} +{% block content %} +

    IMDb News

    + +
      + {% for n in items %} +
    • + {{ n.headline }} + — {{ n.source }} · {{ n.published_at }} · {{ n.category }} +

      {{ n.summary }}

      + {% if n.related_tt and n.related_tt.startswith('tt') %} + View related title › + {% elif n.related_tt and n.related_tt.startswith('nm') %} + View related person › + {% endif %} +
    • + {% endfor %} +
    +{% endblock %} diff --git a/sites/imdb/templates/news_detail.html b/sites/imdb/templates/news_detail.html new file mode 100644 index 00000000..97419c54 --- /dev/null +++ b/sites/imdb/templates/news_detail.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% block title %}{{ item.headline }} - IMDb{% endblock %} +{% block content %} +IMDb News +
    +

    {{ item.headline }}

    +

    {{ item.source }} ¡ {{ item.published_at }} ¡ {{ item.category }}

    +

    {{ item.summary }}

    + {% if item.related_tt.startswith('tt') %} + View related title › + {% elif item.related_tt.startswith('nm') %} + View related person › + {% endif %} +
    +{% endblock %} diff --git a/sites/imdb/templates/offline_service.html b/sites/imdb/templates/offline_service.html new file mode 100644 index 00000000..398d82ae --- /dev/null +++ b/sites/imdb/templates/offline_service.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% block content %} +‹ IMDb home +

    {{ heading }}

    +{% if service == 'birthdays' %}

    The catalog records birth years, but does not contain verified birth dates. A “born today” list cannot be generated reliably from those records.

    +{% elif service == 'calendar' %}

    Live showtimes, episode schedules and upcoming release calendars are not included in this catalog snapshot.

    +{% else %}

    This service requires the live IMDb site and is not available in the offline benchmark. No external account, subscription or payment is required to use this mirror.

    {% endif %} +

    You can still search titles and people, browse charts, rate titles, write reviews and manage your Watchlist locally.

    +Browse titles +Your Watchlist +{% endblock %} diff --git a/sites/imdb/templates/register.html b/sites/imdb/templates/register.html new file mode 100644 index 00000000..d7c67772 --- /dev/null +++ b/sites/imdb/templates/register.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} +{% block title %}Create an account - IMDb{% endblock %} +{% block content %} +

    Create your account

    +
    + + + + + +
    +

    Already have an account? Sign in.

    +{% endblock %} diff --git a/sites/imdb/templates/search.html b/sites/imdb/templates/search.html new file mode 100644 index 00000000..c71af47c --- /dev/null +++ b/sites/imdb/templates/search.html @@ -0,0 +1,46 @@ +{% extends 'base.html' %} +{% block title %}{% if q %}{{ q }} — {% endif %}IMDb search{% endblock %} +{% block content %} +

    Search results{% if q %} for “{{ q }}”{% endif %}

    +

    + All | + Titles ({{ titles|length }}) | + Names ({{ people|length }}) +

    + +{% if titles %} +
    +

    Titles

    +
      + {% for t in titles %} +
    • + {{ t.primary_title }} + ({{ t.year }}) + · {{ t.title_type }} + · {{ '%.1f'|format(t.rating_avg) }} + · {{ t.num_votes|compact_votes }} votes + +
    • + {% endfor %} +
    +
    +{% endif %} + +{% if people %} +
    +

    Names

    +
      + {% for p in people %} +
    • + {{ p.name }} + · {{ p.primary_profession }} +
    • + {% endfor %} +
    +
    +{% endif %} + +{% if q and not titles and not people %} +

    No results matching “{{ q }}”.

    +{% endif %} +{% endblock %} diff --git a/sites/imdb/templates/title_detail.html b/sites/imdb/templates/title_detail.html new file mode 100644 index 00000000..31afaaed --- /dev/null +++ b/sites/imdb/templates/title_detail.html @@ -0,0 +1,170 @@ +{% extends 'base.html' %} +{% block title %}{{ title.primary_title }} ({{ title.year }}) - IMDb{% endblock %} +{% block content %} + +
    +
    + +
    +
    +

    {{ title.primary_title }}

    +
    + {% if title.title_type == 'tvSeries' %}TV Series · {% endif %} + {{ title.year }}{% if title.end_year and title.end_year != title.year %}–{{ title.end_year }}{% endif %} + · {{ title.mpaa_rating or 'Not Rated' }} + {% if title.runtime_min %}· {{ title.runtime_min }}m{% endif %} +
    +
    + {% for g in title.genres %} + {{ g.name }} + {% endfor %} +
    +
    +
    +
    +
    IMDb Rating
    +
    + + {{ '%.1f'|format(title.rating_avg) }}/10 +
    +
    {{ title.num_votes|compact_votes }}
    +
    + {% if title.top_rank %} +
    #{{ title.top_rank }} in Top 250
    + {% endif %} + {% if title.popularity_rank %} +
    Popularity #{{ title.popularity_rank }}
    + {% endif %} +
    +
    + +
    + {% if title.poster_path %} + + {% endif %} + +
    + {% if title.taglines() %} +

    “{{ title.taglines()[0] }}”

    + {% endif %} + +
    +

    Plot

    + {% if title.plot_short and title.plot_short != title.plot %}

    {{ title.plot_short }}

    {% endif %} +

    {{ title.plot }}

    +
    + +
    +
    +
    + +
    +

    Top cast

    + {% if cast %} +
      + {% for c in cast %} +
    • + {% if c.person.photo_path %} + + {% endif %} + {{ c.person.name }} + {% if c.character %}
      as {{ c.character }}
      {% endif %} +
    • + {% endfor %} +
    +

    See full cast & crew ›

    + {% else %} +

    No cast listed.

    + {% endif %} + + {% if title.directors %} +

    Director{{ 's' if title.directors|length > 1 else '' }}: + {% for c in title.directors %} + {{ c.person.name }}{{ ', ' if not loop.last }} + {% endfor %} +

    + {% endif %} + {% if title.writers %} +

    Writer{{ 's' if title.writers|length > 1 else '' }}: + {% for c in title.writers %} + {{ c.person.name }}{{ ', ' if not loop.last }} + {% endfor %} +

    + {% endif %} +
    + +
    +

    Box office & details

    + +
    + + + +
    +

    Featured reviews

    + {% for r in featured_reviews %} +
    +
    + {{ r.headline }} + {% if r.rating %}{{ r.rating }}/10{% endif %} +
    +

    {{ r.body }}

    +
    by {{ r.user.name }} · {{ r.helpful_count }} found this helpful
    +
    + {% else %} +

    No reviews yet.

    + {% endfor %} +

    All reviews ({{ title.reviews|length }}) ›

    +
    + + {% if similar %} +
    +

    More like this

    +
      + {% for s in similar %} +
    • + {{ s.primary_title }} +
      {{ '%.1f'|format(s.rating_avg) }} · {{ s.year }}
      +
    • + {% endfor %} +
    +
    + {% endif %} +
    +{% endblock %} diff --git a/sites/imdb/templates/title_fullcredits.html b/sites/imdb/templates/title_fullcredits.html new file mode 100644 index 00000000..99dd5f50 --- /dev/null +++ b/sites/imdb/templates/title_fullcredits.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} +{% block title %}Full cast & crew — {{ title.primary_title }}{% endblock %} +{% block content %} +‹ {{ title.primary_title }} +

    {{ title.primary_title }} — Full cast & crew

    + +{% set roles = [('director', 'Directed by'), ('writer', 'Written by'), + ('producer', 'Produced by'), ('actor', 'Cast'), + ('composer', 'Music by')] %} +{% for role, label in roles %} + {% set members = title.credits|selectattr('role', 'equalto', role)|list %} + {% if members %} +
    +

    {{ label }}

    + + + {% for c in members %} + + + + + {% endfor %} + +
    {{ c.person.name }}{% if c.character %}as {{ c.character }}{% endif %}
    +
    + {% endif %} +{% endfor %} +{% endblock %} diff --git a/sites/imdb/templates/title_reviews.html b/sites/imdb/templates/title_reviews.html new file mode 100644 index 00000000..3bc9ffb7 --- /dev/null +++ b/sites/imdb/templates/title_reviews.html @@ -0,0 +1,37 @@ +{% extends 'base.html' %} +{% block title %}User reviews — {{ title.primary_title }}{% endblock %} +{% block content %} +‹ {{ title.primary_title }} +

    User reviews for {{ title.primary_title }} ({{ title.year }})

    + +
    + Sort by: + Most helpful | + Most recent | + Rating +
    + +{% if current_user.is_authenticated %} +

    Write your review

    +{% endif %} + +
      +{% for r in reviews %} +
    • +
      + {{ r.headline }} + {% if r.rating %}{{ r.rating }}/10{% endif %} +
      +

      {{ r.body }}

      +
      + by {{ r.user.name }} + · {{ r.created_at.strftime('%Y-%m-%d') }} + · {{ r.helpful_count }} found this helpful +
      +
    • +{% else %} +
    • No reviews yet.
    • +{% endfor %} +
    + +{% endblock %} diff --git a/sites/imdb/templates/title_write_review.html b/sites/imdb/templates/title_write_review.html new file mode 100644 index 00000000..10ae91ed --- /dev/null +++ b/sites/imdb/templates/title_write_review.html @@ -0,0 +1,21 @@ +{% extends 'base.html' %} +{% block title %}Write a review — {{ title.primary_title }}{% endblock %} +{% block content %} +

    Write a review for {{ title.primary_title }} ({{ title.year }})

    +
    + + + + + +
    +{% endblock %} diff --git a/sites/imdb/templates/watchlist.html b/sites/imdb/templates/watchlist.html new file mode 100644 index 00000000..9cb8d41e --- /dev/null +++ b/sites/imdb/templates/watchlist.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} +{% block title %}Your Watchlist - IMDb{% endblock %} +{% block content %} +

    {{ current_user.name }}'s Watchlist

    +

    {{ titles|length }} title{{ 's' if titles|length != 1 }} saved.

    +
      + {% for t in titles %} +
    • + {{ t.primary_title }} + ({{ t.year }}) · + {{ '%.1f'|format(t.rating_avg) }} · + {% if t.runtime_min %}{{ t.runtime_min }}m{% endif %} + +
      + + +
      +
    • + {% else %} +
    • Your Watchlist is empty. Visit a title page and click Add to Watchlist.
    • + {% endfor %} +
    +{% endblock %} diff --git a/sites/imdb/tests/test_answer_checks.py b/sites/imdb/tests/test_answer_checks.py new file mode 100644 index 00000000..46020626 --- /dev/null +++ b/sites/imdb/tests/test_answer_checks.py @@ -0,0 +1,65 @@ +"""Synthetic answer-parser boundary cases; these are not browser runs.""" +from pathlib import Path +import sys +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'verify')) +from answer_checks import (displayed_money, entity_texts, has_amount, has_date, + has_number, mentions, money_field) + + +class AnswerChecksTests(unittest.TestCase): + def test_equivalent_units_and_exact_value(self): + for answer in ('$250.9M', 'USD 250.9 million', '$250,933,053'): + with self.subTest(answer=answer): + self.assertTrue(has_amount(answer, 250933053)) + self.assertFalse(has_amount('$250M', 250933053)) + self.assertFalse(has_amount('$250.9M or $250M', 250933053)) + + def test_field_binding_in_either_direction(self): + for answer in ('Budget: $25M; opening weekend: $727.3K', + '$25 million budget and $727,327 opening weekend'): + with self.subTest(answer=answer): + self.assertTrue(money_field(answer, 'budget', 25000000)) + self.assertTrue(money_field(answer, 'opening', 727327)) + swapped = 'Budget: $727.3K; opening weekend: $25M' + self.assertFalse(money_field(swapped, 'budget', 25000000)) + self.assertFalse(money_field(swapped, 'opening', 727327)) + + def test_year_does_not_masquerade_as_money(self): + self.assertTrue(has_amount('The Godfather (1972): $250.9M', 250933053)) + self.assertFalse(has_amount('The Godfather (1972)', 1972)) + + def test_rows_and_columns_bind_entities(self): + entities = {'a': ['The Dark Knight'], 'b': ['Inception']} + tables = [ + '| Title | IMDb rating | Worldwide gross |\n|---|---|---|\n' + '| The Dark Knight | 9.1 | $1.0B |\n| Inception | 8.8 | $839.8M |', + '| Field | The Dark Knight | Inception |\n|---|---|---|\n' + '| IMDb rating | 9.1 | 8.8 |\n| Worldwide gross | $1.0B | $839.8M |', + ] + for table in tables: + with self.subTest(table=table): + records = entity_texts(table, entities) + self.assertTrue(has_number(records['a'], 9.1)) + self.assertFalse(has_number(records['a'], 8.8)) + self.assertTrue(money_field(records['b'], 'worldwide', 839796627)) + self.assertFalse(money_field(records['a'], 'worldwide', 839796627)) + + def test_dates_and_negation(self): + self.assertTrue(has_date('May 26, 2026', '2026-05-26')) + self.assertTrue(has_date('26 May 2026', '2026-05-26')) + self.assertFalse(has_date('May 25, 2026', '2026-05-26')) + self.assertTrue(mentions('Christian Bale as Bruce Wayne', 'Christian Bale')) + self.assertFalse(mentions('It is not Christian Bale', 'Christian Bale')) + self.assertFalse(mentions('Christian Baleish', 'Christian Bale')) + + def test_sentence_final_number_and_decimal_boundaries(self): + self.assertTrue(has_number('Born in 1974.', 1974)) + self.assertTrue(has_number('IMDb rating: 9.0.', 9)) + self.assertFalse(has_number('IMDb rating: 9.01.', 9)) + self.assertFalse(has_number('Code x1974y', 1974)) + + +if __name__ == '__main__': + unittest.main() diff --git a/sites/imdb/tests/test_app.py b/sites/imdb/tests/test_app.py new file mode 100644 index 00000000..54291b64 --- /dev/null +++ b/sites/imdb/tests/test_app.py @@ -0,0 +1,144 @@ +"""Handler regressions against a disposable copy of the packaged seed. + +Run with the Dockerfile dependencies: python sites/imdb/tests/test_app.py +These tests do not replace browser task execution or visual acceptance. +""" +import importlib.util +from html import unescape +from pathlib import Path +import re +import shutil +import sys +import tempfile +import unittest +from urllib.parse import urlsplit + + +class ReviewInputTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + site = Path(__file__).resolve().parents[1] + cls.temp = tempfile.TemporaryDirectory(prefix='imdb-handler-tests-') + root = Path(cls.temp.name) + for filename in ('app.py', 'seed_data.py'): + shutil.copyfile(site / filename, root / filename) + (root / 'templates').symlink_to(site / 'templates', target_is_directory=True) + (root / 'static').symlink_to(site / 'static', target_is_directory=True) + (root / 'instance').mkdir() + shutil.copyfile(site / 'instance_seed/imdb.db', root / 'instance/imdb.db') + sys.path.insert(0, str(root)) + spec = importlib.util.spec_from_file_location('imdb_test_app', root / 'app.py') + cls.module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = cls.module + spec.loader.exec_module(cls.module) + cls.module.app.config['TESTING'] = True + + @classmethod + def tearDownClass(cls): + with cls.module.app.app_context(): + cls.module.db.session.remove() + cls.module.db.engine.dispose() + sys.path.remove(cls.temp.name) + sys.modules.pop('imdb_test_app', None) + cls.temp.cleanup() + + def setUp(self): + self.client = self.module.app.test_client() + response = self.client.post('/login', data={ + 'email': 'alice.j@test.com', 'password': 'TestPass123!', + 'csrf_token': self.csrf_token('/login'), + }) + self.assertEqual(response.status_code, 302) + + def csrf_token(self, path): + response = self.client.get(path) + self.assertEqual(response.status_code, 200) + match = re.search(r'name="csrf_token"\s+value="([^"]+)"', response.get_data(as_text=True)) + self.assertIsNotNone(match, 'The rendered form must contain a CSRF token.') + return match.group(1) + + def review_count(self): + with self.module.app.app_context(): + return self.module.Review.query.count() + + def test_homepage_links_and_media_are_local_and_resolve(self): + response = self.client.get('/') + self.assertEqual(response.status_code, 200) + body = response.get_data(as_text=True) + targets = set(unescape(value) for value in re.findall(r'(?:href|src)="([^"]+)"', body)) + for target in sorted(targets): + with self.subTest(target=target): + self.assertFalse(urlsplit(target).netloc, 'Homepage must not hotlink external assets or routes') + if target.startswith('/'): + with self.client.get(target, follow_redirects=True) as result: + self.assertEqual(result.status_code, 200) + + def test_featured_today_recreates_the_full_sourced_editorial_path(self): + response = self.client.get('/feature/featured-today-1') + self.assertEqual(response.status_code, 200) + body = unescape(response.get_data(as_text=True)) + self.assertIn('Here\'s what to watch in September', body) + self.assertIn('17 titles', body) + self.assertEqual(body.count('data-anticipated-item'), 17) + for expected in ('Hopeu', 'Practical Magic 2', 'Lanterns', 'MobLand', + 'Victorian Psycho'): + self.assertIn(expected, body) + targets = set(unescape(value) for value in re.findall(r'(?:href|src)="([^"]+)"', body)) + for target in sorted(targets): + with self.subTest(target=target): + self.assertFalse(urlsplit(target).netloc, + 'The editorial page must not hotlink external assets or routes') + + def test_featured_today_items_have_local_detail_routes(self): + response = self.client.get('/feature/featured-today-1') + item_paths = sorted(set(re.findall( + r'href="(/feature/featured-today-1/title/tt\d+)"', + response.get_data(as_text=True)))) + self.assertEqual(len(item_paths), 17) + for path in item_paths: + with self.subTest(path=path): + detail = self.client.get(path) + self.assertEqual(detail.status_code, 200) + self.assertIn(b'Back to all 17 titles', detail.data) + self.assertEqual( + self.client.get('/feature/featured-today-1/title/tt0000000').status_code, + 404, + ) + + def test_bootstrap_on_populated_seed_preserves_database_bytes(self): + database = Path(self.temp.name) / 'instance/imdb.db' + before = database.read_bytes() + self.module._bootstrap() + self.assertEqual(database.read_bytes(), before) + + def test_invalid_review_ratings_do_not_write(self): + for rating in ('nonsense', '0', '11', '2.5'): + with self.subTest(rating=rating): + before = self.review_count() + response = self.client.post('/title/tt0816692/review', data={ + 'headline': 'Validation probe', 'body': 'A non-empty review.', + 'rating': rating, + 'csrf_token': self.csrf_token('/title/tt0816692/review'), + }) + self.assertEqual(response.status_code, 200) + self.assertIn(b'Rating must be a whole number from 1 to 10.', response.data) + self.assertEqual(self.review_count(), before) + + def test_valid_optional_and_boundary_review_ratings(self): + for rating in ('', '1', '10'): + with self.subTest(rating=rating): + before = self.review_count() + response = self.client.post('/title/tt0816692/review', data={ + 'headline': 'Valid rating probe', 'body': 'A non-empty review.', + 'rating': rating, + 'csrf_token': self.csrf_token('/title/tt0816692/review'), + }) + self.assertEqual(response.status_code, 302) + self.assertEqual(self.review_count(), before + 1) + with self.module.app.app_context(): + row = self.module.Review.query.order_by(self.module.Review.id.desc()).first() + self.assertEqual(row.rating, int(rating) if rating else None) + + +if __name__ == '__main__': + unittest.main() diff --git a/sites/imdb/tests/test_boundary.py b/sites/imdb/tests/test_boundary.py new file mode 100644 index 00000000..85290a61 --- /dev/null +++ b/sites/imdb/tests/test_boundary.py @@ -0,0 +1,426 @@ +"""Closed S1 handler regressions using only synthetic temporary database rows. + +CSRF stays enabled; tokens come from the real rendered forms. These tests are +not browser task execution or S1 visual/functional acceptance. +""" +from collections import Counter +from datetime import datetime +import importlib.util +from pathlib import Path +import re +import shutil +import sys +import tempfile +import time +import unittest +from unittest.mock import patch + + +class BoundaryTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + site = Path(__file__).resolve().parents[1] + cls.temp = tempfile.TemporaryDirectory(prefix='imdb-synthetic-boundary-tests-') + root = Path(cls.temp.name) + shutil.copyfile(site / 'app.py', root / 'app.py') + (root / 'seed_data.py').write_text('def seed_all(*args):\n pass\n') + (root / 'templates').symlink_to(site / 'templates', target_is_directory=True) + prior_seed = sys.modules.pop('seed_data', None) + sys.path.insert(0, str(root)) + try: + spec = importlib.util.spec_from_file_location('imdb_boundary_test_app', root / 'app.py') + cls.module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = cls.module + spec.loader.exec_module(cls.module) + finally: + sys.modules.pop('seed_data', None) + if prior_seed is not None: + sys.modules['seed_data'] = prior_seed + sys.path.remove(str(root)) + cls.module.app.config['TESTING'] = True + + @classmethod + def tearDownClass(cls): + with cls.module.app.app_context(): + cls.module.db.session.remove() + cls.module.db.engine.dispose() + sys.modules.pop('imdb_boundary_test_app', None) + cls.temp.cleanup() + + def setUp(self): + m = self.module + with m.app.app_context(): + m.db.drop_all() + m.db.create_all() + now = datetime(2020, 1, 1) + for index, name in ((1, 'Alice'), (2, 'Bob')): + user = m.User(id=index, email=f'{name.lower()}@test.com', name=f'{name} synthetic', created_at=now) + user.set_password('Synthetic123!') + m.db.session.add(user) + m.db.session.add(m.Title(id=1, tt_id='ttsynthetic', primary_title='Synthetic title', + year=2000, runtime_min=120, rating_avg=7.0, num_votes=100)) + m.db.session.flush() + for index, rating in ((1, 4), (2, 7)): + m.db.session.add(m.UserRating(id=index, user_id=index, title_id=1, + rating=rating, created_at=now)) + m.db.session.add(m.WatchlistItem(id=index, user_id=index, title_id=1, added_at=now)) + m.db.session.commit() + self.client = m.app.test_client() + + def token(self, path='/login', client=None, base_url=None): + response = (client or self.client).get(path, base_url=base_url) + self.assertEqual(response.status_code, 200) + match = re.search(r'name="csrf_token"\s+value="([^"]+)"', response.get_data(as_text=True)) + self.assertIsNotNone(match, 'The rendered form must contain a CSRF token.') + return match.group(1) + + def post(self, path, data=None, form_path=None, base_url=None, **kwargs): + data = dict(data or {}) + data['csrf_token'] = self.token(form_path or path, base_url=base_url) + return self.client.post(path, data=data, base_url=base_url, **kwargs) + + def login(self, base_url=None): + response = self.post('/login', {'email': 'alice@test.com', 'password': 'Synthetic123!'}, base_url=base_url) + self.assertEqual(response.status_code, 302) + + def actor(self, base_url=None): + with self.client.session_transaction(base_url=base_url) as session: + return session.get('_user_id') + + def test_home_and_news_sort_by_publication_date_and_filter_category(self): + m = self.module + with m.app.app_context(): + m.db.session.add_all([ + m.NewsItem(id=1, headline='Newest film news', published_at='2025-02-14', category='Movies'), + m.NewsItem(id=9, headline='Old television news', published_at='2024-01-01', category='TV'), + ]) + m.db.session.commit() + for path in ('/', '/news'): + body = self.client.get(path).get_data(as_text=True) + self.assertLess(body.index('Newest film news'), body.index('Old television news')) + response = self.client.get('/news?category=Movies') + self.assertIn(b'Newest film news', response.data) + self.assertNotIn(b'Old television news', response.data) + self.assertEqual(self.client.get('/news/1').status_code, 200) + self.assertEqual(self.client.get('/news/999').status_code, 404) + + def test_recently_viewed_is_session_local_and_clear_requires_csrf(self): + self.assertEqual(self.client.get('/title/ttsynthetic').status_code, 200) + with self.client.session_transaction() as state: + self.assertEqual(state['recent_titles'], ['ttsynthetic']) + self.client.get('/title/ttsynthetic') + with self.client.session_transaction() as state: + self.assertEqual(state['recent_titles'], ['ttsynthetic']) + other = self.module.app.test_client() + self.assertIn(b'You have no recently viewed pages', other.get('/').data) + self.assertEqual(self.client.post('/recently-viewed/clear').status_code, 400) + self.assertEqual(self.client.get('/recently-viewed/clear').status_code, 405) + response = self.client.post('/recently-viewed/clear', data={'csrf_token': self.token('/')}) + self.assertEqual(response.status_code, 302) + self.assertIn(b'You have no recently viewed pages', self.client.get('/').data) + + def test_home_cards_use_authenticated_watchlist_state(self): + m = self.module + with m.app.app_context(): + title = m.db.session.get(m.Title, 1) + title.top_rank = 1 + m.db.session.commit() + self.assertIn(b'Sign in to add Synthetic title', self.client.get('/').data) + self.login() + body = self.client.get('/').data + self.assertIn(b'Remove Synthetic title from Watchlist', body) + self.assertIn(b'In Watchlist', body) + response = self.client.post('/title/ttsynthetic/watchlist', data={'csrf_token': self.token('/')}, + headers={'Referer': 'http://localhost/'}) + self.assertEqual(response.location, '/') + self.assertIn(b'Add Synthetic title to Watchlist', self.client.get('/').data) + + def test_home_feature_escapes_source_text_and_unknown_features_404(self): + m = self.module + with m.app.app_context(): + m.db.session.add(m.HomeFeature(id='synthetic', kind='topic', position=1, + heading='', subtitle='Source description', + source_url='https://www.imdb.com/example/', captured_at='2026-09-10', + payload={})) + m.db.session.commit() + for path in ('/', '/feature/synthetic'): + body = self.client.get(path).data + self.assertIn(b'<script>alert(1)</script>', body) + self.assertNotIn(b'', body) + self.assertEqual(self.client.get('/feature/missing').status_code, 404) + + def snapshot(self): + # Rows, including password hashes, are compared in memory, never logged. + with self.module.app.app_context(): + return {table.name: Counter(tuple(row) for row in self.module.db.session.execute(table.select())) + for table in self.module.db.metadata.sorted_tables} + + def test_snapshot_collections_are_dated_local_and_do_not_invent_missing_facts(self): + m = self.module + with m.app.app_context(): + for kind, payload in [('streaming', {'source_id': 'ttsynthetic', 'service': 'PRIME VIDEO', 'rating': '8.0'}), + ('starmeter', {'source_id': 'nm1', 'rank': 7, 'known_for': ['Source film']}), + ('birthday', {'source_id': 'nm1'}), + ('tv_schedule', {'source_id': 'tt2', 'episode': 'New: Season 1', 'air_date': 'Wed, Sep 16'})]: + m.db.session.add(m.HomeFeature(id=kind + '-test', kind=kind, position=1, + heading='Source ' + kind, subtitle='Source data', image_path='images/test.jpg', + poster_path='images/test.jpg', source_url='https://www.imdb.com/', + captured_at='2026-09-10', payload=payload)) + m.db.session.commit() + for kind in ('streaming', 'birthday', 'tv_schedule', 'starmeter'): + for path in ('/discover/' + kind, '/feature/' + kind + '-test'): + response = self.client.get(path) + self.assertEqual(response.status_code, 200) + self.assertIn(b'2026-09-10', response.data) + self.assertIn(('Source ' + kind).encode(), response.data) + detail = self.client.get('/feature/streaming-test').data + self.assertIn(b'/title/ttsynthetic', detail) + self.assertIn(b'not available in this offline mirror', detail) + self.assertNotIn(b'Age at capture', self.client.get('/feature/birthday-test').data) + chart_detail = self.client.get('/feature/starmeter-test').data + self.assertIn(b'STARmeter rank at capture
    7
    ', chart_detail) + self.assertIn(b'Source film', chart_detail) + self.assertNotIn(b'Add to favorite', chart_detail) + self.assertIn(b'person-rank">7', self.client.get('/').data) + self.assertNotIn(b'IMDb rating at capture', self.client.get('/feature/tv_schedule-test').data) + self.assertEqual(self.client.get('/discover/unknown').status_code, 404) + + def test_home_release_and_favorite_ordering_use_real_fields(self): + m = self.module + with m.app.app_context(): + title = m.db.session.get(m.Title, 1) + title.release_date = '2025-02-01' + m.db.session.add(m.Title(id=2, tt_id='ttsecond', primary_title='Second release', + release_date='2025-01-01', num_votes=1000, rating_avg=9.0)) + m.db.session.add(m.Title(id=3, tt_id='ttinvalid', primary_title='Unparsed release', + release_date='Release date | 2027', num_votes=1, rating_avg=8.0)) + m.db.session.commit() + body = self.client.get('/').get_data(as_text=True) + releases = body.split('Latest catalog releases', 1)[1].split('', 1)[0] + self.assertLess(releases.index('Synthetic title'), releases.index('Second release')) + self.assertNotIn('Unparsed release', releases) + favorites = body.split('Fan favorites', 1)[1].split('', 1)[0] + self.assertLess(favorites.index('Second release'), favorites.index('Synthetic title')) + + def assert_unchanged(self, before): + after = self.snapshot() + self.assertEqual([name for name in before if before[name] != after[name]], []) + + def test_all_post_routes_reject_missing_and_invalid_csrf(self): + targets = ['/login', '/register', '/title/ttsynthetic/rate', + '/title/ttsynthetic/review', '/title/ttsynthetic/watchlist', '/logout'] + self.login() + for target in targets: + for supplied in ({}, {'csrf_token': 'invalid'}): + with self.subTest(target=target, supplied=bool(supplied)): + before = self.snapshot() + response = self.client.post(target, data={ + 'email': 'new@test.com', 'name': 'New', 'password': 'Synthetic123!', + 'rating': '8', 'headline': 'Synthetic review', 'body': 'Synthetic body.', **supplied}) + self.assertEqual(response.status_code, 400) + self.assertIn(b'CSRF', response.data) + self.assertEqual(self.actor(), '1') + self.assert_unchanged(before) + + def test_login_and_registration_without_session_reject_missing_csrf(self): + for target in ('/login', '/register'): + before = self.snapshot() + response = self.client.post(target, data={'email': 'new@test.com', 'name': 'New', + 'password': 'Synthetic123!'}) + self.assertEqual(response.status_code, 400) + self.assertIsNone(self.actor()) + self.assert_unchanged(before) + + def test_other_session_token_is_rejected(self): + other = self.module.app.test_client() + other_token = self.token(client=other) + self.login() + before = self.snapshot() + response = self.client.post('/title/ttsynthetic/rate', data={'rating': '8', 'csrf_token': other_token}) + self.assertEqual(response.status_code, 400) + self.assert_unchanged(before) + + def test_token_expiry_remains_enabled(self): + # Only age the GET-issued signatures: the session remains within its + # lifetime while the form token exceeds Flask-WTF's unchanged limit. + with patch('itsdangerous.timed.TimestampSigner.get_timestamp', return_value=int(time.time()) - 7200): + token = self.token() + before = self.snapshot() + response = self.client.post('/login', data={'email': 'alice@test.com', + 'password': 'Synthetic123!', 'csrf_token': token}) + self.assertEqual(response.status_code, 400) + self.assertIn(b'expired', response.data) + self.assertIsNone(self.actor()) + self.assert_unchanged(before) + + def test_cross_origin_without_token_cannot_mutate(self): + base = 'http://localhost:48015' + self.login(base_url=base) + for action in ('rate', 'review', 'watchlist'): + before = self.snapshot() + response = self.client.post('/title/ttsynthetic/' + action, base_url=base, + headers={'Origin': 'http://localhost:48016', + 'Referer': 'http://localhost:48016/form'}, + data={'rating': '8', 'headline': 'Synthetic', 'body': 'Body'}) + self.assertEqual(response.status_code, 400) + self.assert_unchanged(before) + + def test_real_form_tokens_allow_normal_writes_and_preserve_ownership(self): + self.login() + m = self.module + with m.app.app_context(): + old = m.UserRating.query.filter_by(user_id=1).one() + identity = old.id, old.created_at + response = self.post('/title/ttsynthetic/rate', {'rating': '8', 'user_id': '2'}, + form_path='/title/ttsynthetic') + self.assertEqual(response.status_code, 302) + with m.app.app_context(): + old = m.UserRating.query.filter_by(user_id=1).one() + self.assertEqual((old.id, old.created_at), identity) + self.assertEqual(old.rating, 8) + self.assertEqual(m.UserRating.query.filter_by(user_id=2).one().rating, 7) + response = self.post('/title/ttsynthetic/review', {'headline': 'Synthetic review', 'body': 'Body.', + 'rating': '10', 'user_id': '2', 'helpful_count': '999', 'is_seed': '1'}) + self.assertEqual(response.status_code, 302) + with m.app.app_context(): + row = m.Review.query.one() + self.assertEqual((row.user_id, row.title_id, row.rating, row.helpful_count, row.is_seed), (1, 1, 10, 0, False)) + response = self.post('/title/ttsynthetic/watchlist', {'user_id': '2'}, form_path='/list/watchlist') + self.assertEqual(response.status_code, 302) + with m.app.app_context(): + self.assertEqual(m.WatchlistItem.query.filter_by(user_id=1).count(), 0) + self.assertEqual(m.WatchlistItem.query.filter_by(user_id=2).count(), 1) + response = self.post('/title/ttsynthetic/watchlist', form_path='/title/ttsynthetic') + self.assertEqual(response.status_code, 302) + with m.app.app_context(): + self.assertEqual(m.WatchlistItem.query.filter_by(user_id=1).count(), 1) + + def test_logout_requires_token_post_and_get_head_preserve_session(self): + self.login() + before = self.snapshot() + for method in ('GET', 'HEAD'): + response = self.client.open('/logout', method=method) + self.assertEqual(response.status_code, 405) + self.assertEqual(self.actor(), '1') + response = self.post('/logout', form_path='/account') + self.assertEqual(response.status_code, 302) + self.assertIsNone(self.actor()) + self.assert_unchanged(before) + + def test_account_body_logout_form_ends_session_without_data_changes(self): + self.login() + before = self.snapshot() + page = self.client.get('/account').get_data(as_text=True) + self.assertNotRegex(page, r']*href="/logout"') + forms = [(attributes, body) for attributes, body in + re.findall(r']*)>(.*?)', page, re.S) + if 'action="/logout"' in attributes] + self.assertEqual(len(forms), 2, 'Both navigation and account-body exits must submit forms.') + attributes, body = forms[-1] + self.assertIn('method="post"', attributes) + token = re.search(r'name="csrf_token"\s+value="([^"]+)"', body) + self.assertIsNotNone(token) + response = self.client.post('/logout', data={'csrf_token': token.group(1)}) + self.assertEqual(response.status_code, 302) + self.assertIsNone(self.actor()) + self.assert_unchanged(before) + + def test_watchlist_referrers_preserve_only_same_origin_path_query(self): + base = 'http://localhost:48015' + self.login(base_url=base) + cases = [ + (base + '/list/watchlist?sort=recent#ignored', '/list/watchlist?sort=recent'), + ('http://LOCALHOST:48015/list/watchlist', '/list/watchlist'), + ('http://localhost:48015', '/'), + ('http://localhost:48016/list/watchlist', '/title/ttsynthetic'), + ('https://localhost:48015/list/watchlist', '/title/ttsynthetic'), + ('https://example.invalid/landing', '/title/ttsynthetic'), + ('//example.invalid/landing', '/title/ttsynthetic'), + ('//localhost:48015/list/watchlist', '/title/ttsynthetic'), + ('http://alice@localhost:48015/list/watchlist', '/title/ttsynthetic'), + ('http://localhost:48015@evil.invalid/landing', '/title/ttsynthetic'), + ('http://localhost:invalid/landing', '/title/ttsynthetic'), + ('http://localhost:99999/landing', '/title/ttsynthetic'), + ('http://localhost:48015\\@evil.invalid/landing', '/title/ttsynthetic'), + ('http://localhost:48015/\\evil.invalid/landing', '/title/ttsynthetic'), + ('http://localhost:48015//evil.invalid/landing', '/title/ttsynthetic'), + ('http://localhost:48015/%2f%2fevil.invalid', '/title/ttsynthetic'), + ('http://localhost:48015/%5cevil.invalid', '/title/ttsynthetic'), + ('http://localhost:48015/list/%09watchlist', '/title/ttsynthetic'), + ('http://localhost:48015/list/\twatchlist', '/title/ttsynthetic'), + ] + for referrer, expected in cases: + with self.subTest(referrer=repr(referrer)): + response = self.post('/title/ttsynthetic/watchlist', form_path='/title/ttsynthetic', base_url=base, + headers={'Referer': referrer}) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.location, expected) + + def test_referrer_default_ports_and_local_ipv6_are_parsed(self): + for base, referrer, expected in ( + ('http://localhost', 'http://localhost:80/list/watchlist?q=1', '/list/watchlist?q=1'), + ('http://localhost:80', 'http://localhost/list/watchlist', '/list/watchlist'), + ('http://[::1]:48015', 'http://[::1]:48015/list/watchlist', '/list/watchlist'), + ('http://task.localhost:48015', 'http://task.localhost:48015/list/watchlist', '/list/watchlist')): + self.client = self.module.app.test_client() + self.login(base_url=base) + response = self.post('/title/ttsynthetic/watchlist', form_path='/title/ttsynthetic', base_url=base, + headers={'Referer': referrer}) + self.assertEqual(response.location, expected) + + def test_invalid_years_return_clear_400_without_database_changes(self): + before = self.snapshot() + for key in ('year_from', 'year_to'): + for value in ('9223372036854775808', '-9223372036854775809', '0', '10000', 'nonsense', '2.5'): + with self.subTest(key=key, value=value): + response = self.client.get('/search/title', query_string={key: value}) + self.assertEqual(response.status_code, 400) + self.assertIn(b'year', response.data.lower()) + self.assert_unchanged(before) + + def test_blank_and_supported_years_preserve_search(self): + before = self.snapshot() + for query in ({}, {'year_from': '', 'year_to': ' '}, {'year_from': '1', 'year_to': '9999'}, + {'year_from': '2000', 'year_to': '2000'}): + response = self.client.get('/search/title', query_string=query) + self.assertEqual(response.status_code, 200) + self.assertIn(b'Synthetic title', response.data) + response = self.client.get('/search/title', query_string={'year_from': '2001'}) + self.assertEqual(response.status_code, 200) + self.assertNotIn(b'Synthetic title', response.data) + self.assert_unchanged(before) + + def test_registration_email_validation_is_offline_and_normalized(self): + for value in ('not-an-email', 'a@@test.com', 'a b@test.com'): + before = self.snapshot() + response = self.post('/register', {'email': value, 'name': 'New', 'password': 'Synthetic123!'}) + self.assertEqual(response.status_code, 200) + self.assertIn(b'valid email', response.data) + self.assertIsNone(self.actor()) + self.assert_unchanged(before) + # Any unintended DNS/network lookup must fail this synthetic test. + with (patch('socket.getaddrinfo', side_effect=AssertionError('No network allowed')), + patch('dns.resolver.Resolver.resolve', side_effect=AssertionError('No DNS allowed'))): + response = self.post('/register', {'email': ' NEW@TEST.COM ', 'name': ' New ', + 'password': 'Synthetic123!'}) + self.assertEqual(response.status_code, 302) + with self.module.app.app_context(): + row = self.module.User.query.filter_by(email='new@test.com').one() + self.assertEqual(row.name, 'New') + new_id = str(row.id) + self.assertEqual(self.actor(), new_id) + + def test_duplicate_registration_and_bad_login_remain_rejected(self): + before = self.snapshot() + response = self.post('/register', {'email': ' ALICE@TEST.COM ', 'name': 'Duplicate', + 'password': 'Synthetic123!'}) + self.assertEqual(response.status_code, 200) + self.assertIn(b'already exists', response.data) + response = self.post('/login', {'email': 'alice@test.com', 'password': 'bad'}) + self.assertEqual(response.status_code, 200) + self.assertIsNone(self.actor()) + self.assert_unchanged(before) + + +if __name__ == '__main__': + unittest.main() diff --git a/sites/imdb/tests/test_expansion_account_reads.py b/sites/imdb/tests/test_expansion_account_reads.py new file mode 100644 index 00000000..0047929e --- /dev/null +++ b/sites/imdb/tests/test_expansion_account_reads.py @@ -0,0 +1,322 @@ +"""Synthetic account-read fixtures; none of these are browser executions.""" + +import hashlib +import json +import sqlite3 +import unittest + +import test_read_tasks as catalog +from expansion_account_reads import check_account_read_task +from verify_lib import RunEvidence, VerificationError + + +PASSWORD = "TestPass123!" +HASH = "pbkdf2:sha256:1000$salt$" + hashlib.pbkdf2_hmac( + "sha256", PASSWORD.encode(), b"salt", 1000).hex() + +ANSWER21 = """| Account | Film | Personal rating | Director | Runtime | +|---|---|---|---|---| +| Alice | The Godfather | 9/10 | Francis Ford Coppola | 175 minutes | +| Alice | Inception | 10/10 | Christopher Nolan | 148 minutes | +| Bob | The Dark Knight | 10/10 | Christopher Nolan | 152 minutes | +| Bob | Synthetic Cutoff Echo | 9/10 | Synthetic Director | 100 minutes | +""" +ANSWER22 = """The Dark Knight (2008) — repeated headline: Same words. + +| Author | Review score | Review date | Helpful count | +|---|---|---|---| +| Alice Johnson | 9/10 | 2020-02-03 | 42 | +| DustBowlPilot | 7/10 | 2021-03-04 | 18 | +""" + + +class SyntheticAccountReadTests(unittest.TestCase): + def setUp(self): + self.fixture = catalog.SyntheticReadTaskTests("runTest") + self.fixture.setUp() + self.addCleanup(self.fixture.doCleanups) + self.directory = self.fixture.run_dir + self.origin = self.fixture.origin + self.change("UPDATE users SET email='alice.j@test.com',name='Alice Johnson',password_hash=? WHERE id=1", (HASH,)) + self.change("UPDATE users SET email='bob.c@test.com',name='Bob Carter',password_hash=? WHERE id=2", (HASH,)) + self.change("INSERT INTO users VALUES (3,'pilot@example.test','DustBowlPilot',?,'2020-01-01')", (HASH,)) + for row in [(1, 1, 9), (1, 2, 9), (1, 4, 10), (1, 5, 7), + (2, 3, 10), (2, 2, 8), (2, 5, 9), (2, 6, 9)]: + self.change("INSERT INTO user_ratings(user_id,title_id,rating,created_at) VALUES (?,?,?,'2024-01-01')", row) + for row in [(1, 1), (1, 3), (1, 6), (2, 1), (2, 5), (2, 4)]: + self.change("INSERT INTO watchlist_items(user_id,title_id,added_at) VALUES (?,?,'2024-01-01')", row) + for row in [(3, 1, 9, "Same words", 42, "2020-02-03"), + (3, 3, 7, "Same words", 18, "2021-03-04"), + (1, 1, 10, "Different first", 11, "2020-01-01"), + (1, 3, 8, "Different second", 13, "2022-01-01")]: + self.change("INSERT INTO reviews(title_id,user_id,rating,headline,helpful_count,created_at,body,is_seed) " + "VALUES (?,?,?,?,?,?,'Synthetic review',1)", row) + + def change(self, sql, values=(), after_only=False): + names = ("after.db",) if after_only else ("before.db", "after.db") + for name in names: + with sqlite3.connect(self.directory / name) as db: + db.execute(sql, values) + + def step(self, path, action="observe", params=None, after=None, success=True): + step = {"url": self.origin + path, "action": action, "params": params or {}, + "status": "completed", "action_result": {"success": success}} + if after is not None: + step["url_after"] = self.origin + after + return step + + def account_steps(self, name): + email = {"alice": "alice.j@test.com", "bob": "bob.c@test.com"}[name] + return [self.step("/login", "fill", {"label": "Email", "value": email}), + self.step("/login", "fill", {"label": "Password", "value": PASSWORD}), + self.step("/login", "click", {"role": "button", "name": "Sign in"}, "/"), + self.step("/list/ratings"), self.step("/list/watchlist")] + + def steps(self, number): + steps = self.account_steps("alice") + if number == 21: + steps += [self.step("/list/watchlist", "click", {"role": "button", "name": "Sign out", "method": "POST"}, "/")] + steps += self.account_steps("bob") + steps += [self.step("/title/" + identifier) + for identifier in (catalog.GOD, catalog.INCEPTION, catalog.DARK, catalog.ECHO)] + else: + steps += [self.step("/title/" + catalog.DARK + "/reviews?sort=recent")] + return steps + + def check(self, number, answer=None, steps=None): + task_id = f"IMDb--{number}" + question = catalog.TASKS[task_id]["ques"] + row = {"task_id": task_id, "task": question, "start_url": self.origin + "/", + "steps": self.steps(number) if steps is None else steps, + "final_answer": {21: ANSWER21, 22: ANSWER22}[number] if answer is None else answer, + "fixture_kind": "synthetic"} + (self.directory / "trajectory.json").write_text(json.dumps(row)) + with RunEvidence(self.directory, task_id, question) as run: + return check_account_read_task(number, run) + + def rejects(self, number, answer=None, steps=None): + with self.assertRaises(VerificationError): + self.check(number, answer, steps) + + def test_complete_attributed_account_table_needs_no_unrequested_years(self): + self.check(21) + + def test_account_heading_sections_with_prose_are_valid(self): + self.check(21, """Alice + +The Godfather: personal rating 9/10; directed by Francis Ford Coppola; runtime 175 minutes. +Inception: personal rating 10/10; directed by Christopher Nolan; runtime 148 minutes. + +Bob + +The Dark Knight: personal rating 10/10; directed by Christopher Nolan; runtime 152 minutes. +Synthetic Cutoff Echo: personal rating 9/10; directed by Synthetic Director; runtime 100 minutes. +""") + + def test_both_source_orders_and_fullcredits_are_valid(self): + steps = self.account_steps("bob") + steps += [self.step("/list/watchlist", "click", {"role": "button", "name": "Sign out", "method": "POST"}, "/")] + steps += list(self.account_steps("alice")) + steps += [self.step("/title/" + identifier + "/fullcredits") + for identifier in (catalog.ECHO, catalog.DARK, catalog.INCEPTION, catalog.GOD)] + self.check(21, steps=steps) + + def test_wrong_owner_partial_result_global_score_and_swapped_runtime_fail(self): + bad = [ANSWER21.replace("| Alice | Inception", "| Bob | Inception"), + ANSWER21.replace("| Bob | Synthetic Cutoff Echo | 9/10 | Synthetic Director | 100 minutes |", ""), + ANSWER21.replace("9/10 | Francis", "9.2/10 | Francis"), + ANSWER21.replace("175 minutes", "148 minutes"), + ANSWER21.replace("Francis Ford Coppola", "Christopher Nolan"), + ANSWER21.replace("10/10 | Christopher Nolan | 152", "8/10 | Christopher Nolan | 152")] + for answer in bad: + with self.subTest(answer=answer): + self.rejects(21, answer) + + def test_eligible_set_cannot_include_an_existing_watchlist_item(self): + extra = "| Alice | The Shawshank Redemption | 9/10 | Frank Darabont | 142 minutes |\n" + self.rejects(21, ANSWER21 + extra) + + def test_missing_source_list_or_wrong_authentication_fails(self): + for number in (21, 22): + with self.subTest(number=number): + steps = [s for s in self.steps(number) if not s["url"].endswith("/list/ratings")] + self.rejects(number, steps=steps) + steps = self.steps(number) + steps[1]["params"]["value"] = "incorrect-password" + self.rejects(number, steps=steps) + steps = self.steps(number) + steps[2]["action_result"]["success"] = False + self.rejects(number, steps=steps) + + def test_supplied_password_redaction_uses_snapshot_hash_and_successful_login(self): + for number in (21, 22): + with self.subTest(number=number): + steps = self.steps(number) + for step in steps: + if step["params"].get("label") == "Password": + step["params"]["value"] = "[SUPPLIED PASSWORD]" + self.check(number, steps=steps) + + def test_review_table_beneath_movie_heading_is_valid(self): + self.check(22) + + def test_review_prose_and_written_dates_are_valid(self): + self.check(22, """The Dark Knight (2008), repeated headline: Same words. +Alice Johnson: review score 9/10; February 3, 2020; 42 found this helpful. +DustBowlPilot: review score 7/10; 4 March 2021; 18 helpful. +""") + + def test_review_summary_intro_is_not_misread_as_an_unknown_author(self): + self.check(22, "Alice's deduplicated Watchlist union My ratings contains six movies. " + "Exactly one repeated-headline group appears:\n\n" + ANSWER22) + + def test_title_column_in_review_table_is_valid(self): + self.check(22, """Repeated headline: Same words. +| Film | Year | Author | Review score | Review date | Helpful count | +|---|---|---|---|---|---| +| The Dark Knight | 2008 | Alice Johnson | 9/10 | 2020-02-03 | 42 | +| The Dark Knight | 2008 | DustBowlPilot | 7/10 | 2021-03-04 | 18 | +""") + + def test_any_review_sort_is_valid_but_details_only_lack_review_dates(self): + for sort in ("helpful", "rating", "recent"): + steps = self.account_steps("alice") + [self.step("/title/" + catalog.DARK + "/reviews?sort=" + sort)] + self.check(22, steps=steps) + self.rejects(22, steps=self.account_steps("alice") + [self.step("/title/" + catalog.DARK)]) + + def test_swapped_author_dates_scores_and_helpful_counts_fail(self): + changes = [("9/10", "7/10"), ("2020-02-03", "2021-03-04"), + ("| 42 |", "| 18 |"), ("Alice Johnson", "Bob Carter"), + ("Same words", "Different first"), ("(2008)", "(2007)")] + for before, after in changes: + with self.subTest(field=before): + self.rejects(22, ANSWER22.replace(before, after)) + + def test_omitted_review_row_or_field_fails(self): + for part in ("| DustBowlPilot | 7/10 | 2021-03-04 | 18 |", "9/10", "2020-02-03", "42"): + with self.subTest(part=part): + self.rejects(22, ANSWER22.replace(part, "")) + + def test_extra_review_author_rows_fail(self): + for author in ("Bob Carter", "An Invented Author"): + self.rejects(22, ANSWER22 + f"| {author} | 7/10 | 2021-03-04 | 18 |\n") + + def test_equal_headline_on_another_movie_does_not_join_group(self): + self.change("UPDATE reviews SET headline='Same words' WHERE title_id=1 AND user_id=1") + self.check(22) + + def test_same_author_repetition_without_distinct_authors_is_not_a_group(self): + self.change("UPDATE reviews SET user_id=1 WHERE title_id=3") + self.rejects(22) + + def test_closed_21_extra_tv_outside_union_and_unknown_results_fail(self): + for title in ("Synthetic Crime Series", "Synthetic Writer Only Feature", "Invented Extra Film"): + for extra in (f"Alice: Also include {title} as a qualifying movie.", + f"| Alice | {title} | 10/10 | Christopher Nolan | 148 minutes |"): + with self.subTest(extra=extra): + self.rejects(21, ANSWER21 + extra) + self.check(21, ANSWER21 + "Alice: Synthetic Crime Series is excluded because it is a TV series.") + + def test_closed_21_equivalent_runtime_units_and_contradictions(self): + answer = ANSWER21 + for minutes, hours in ((175, "2h 55m"), (148, "2h 28m"), (152, "2 hours 32 minutes"), (100, "1h 40m")): + answer = answer.replace(f"{minutes} minutes", hours) + self.check(21, answer) + self.rejects(21, answer.replace("2h 55m", "2h 54m")) + self.rejects(21, answer.replace("2h 55m", "2h 55m (174 minutes)")) + + def test_closed_scores_accept_rated_out_of_ten_and_global_context(self): + answer = ANSWER21.replace("9/10", "rated 9 out of 10").replace("10/10", "rated 10 out of 10") + self.check(21, answer) + self.check(21, ANSWER21.replace("9/10 | Francis", "9/10 (IMDb rating is 9.2/10) | Francis")) + self.rejects(21, ANSWER21.replace("9/10 | Francis", "8/10 (IMDb rating is 9/10) | Francis")) + self.check(22, ANSWER22.replace("9/10", "rated 9 out of 10")) + self.rejects(22, ANSWER22.replace("9/10", "rated 8 out of 10")) + + def test_closed_22_headline_exactness_and_extra_group(self): + self.rejects(22, ANSWER22.replace("Same words", "Same words with extras")) + self.rejects(22, ANSWER22 + ANSWER22.replace("Same words", "Invented group")) + self.check(22, ANSWER22.replace("Same words.", '\"Same words\".')) + + def add_review(self, score=8, headline="Other words", user=1, date="2022-04-05", helpful=12): + self.change("INSERT INTO reviews(title_id,user_id,rating,headline,helpful_count,created_at,body,is_seed) " + "VALUES (3,?,?,?,?,?,'Synthetic repeated review',1)", + (user, score, headline, helpful, date)) + + def test_closed_22_same_movie_multiple_groups_are_separate(self): + self.add_review() + self.add_review(6, user=3, date="2023-05-06", helpful=14) + other = ANSWER22.replace("Same words", "Other words").replace("9/10", "8/10").replace("7/10", "6/10").replace("2020-02-03", "2022-04-05").replace("2021-03-04", "2023-05-06").replace("42", "12").replace("18", "14") + self.check(22, ANSWER22 + "\n" + other) + self.rejects(22, ANSWER22) + self.rejects(22, ANSWER22 + "\n" + other.replace("Other words", "Same words")) + + def test_closed_22_repeated_author_reviews_require_row_multiplicity(self): + self.add_review(headline="Same words") + extra = "| Alice Johnson | 8/10 | 2022-04-05 | 12 |\n" + self.check(22, ANSWER22 + extra) + self.rejects(22, ANSWER22) + self.rejects(22, ANSWER22 + extra.replace("8/10", "9/10")) + self.add_review(9, "Same words", date="2020-02-03", helpful=42) + self.rejects(22, ANSWER22 + extra) + self.check(22, ANSWER22 + extra + "| Alice Johnson | 9/10 | 2020-02-03 | 42 |\n") + + def test_closed_22_extra_prose_author_and_explicit_exclusion(self): + for author in ("Bob Carter", "An Invented Author"): + self.rejects(22, ANSWER22 + f"Additional matching author: {author}; review score 7/10; 2021-03-04; helpful count 18.") + self.check(22, ANSWER22 + "Bob Carter is excluded; he is not in this matching group.") + + def test_closed_22_unambiguous_author_and_title_aliases(self): + self.check(22, ANSWER22.replace("Alice Johnson", "Alice").replace("The Dark Knight", "Dark Knight")) + self.rejects(22, ANSWER22.replace("Alice Johnson", "Bob")) + self.change("INSERT INTO users VALUES (4,'other@example.test','Alice Smith',?,'2020-01-01')", (HASH,)) + self.rejects(22, ANSWER22.replace("Alice Johnson", "Alice")) + self.check(22, ANSWER22) + + def test_closed_review_multiline_and_single_paragraph_records(self): + heading = "The Dark Knight (2008); repeated headline: Same words.\n" + rows = ["Alice Johnson: review score 9/10; review date 2020-02-03; helpful count 42.", + "DustBowlPilot: review score 7/10; review date 2021-03-04; helpful count 18."] + self.check(22, heading + " ".join(rows)) + multiline = heading + "Alice Johnson\nReview score: 9/10\nReview date: 2020-02-03\nHelpful count: 42\nDustBowlPilot\nReview score: 7/10\nReview date: 2021-03-04\nHelpful count: 18" + self.check(22, multiline) + self.rejects(22, multiline.replace("Review score: 7/10", "Review score: 9/10")) + self.rejects(22, multiline + "\nAn Invented Author: review score 7/10; 2021-03-04; helpful count 18.") + + def test_closed_review_scope_in_headings_and_table_columns(self): + self.check(22, ANSWER22.replace("repeated headline: Same words.", '\"Same words\".')) + self.check(22, "The Dark Knight (2008), repeated headline: Same words.\n" + + "| Film | Author | Review score | Review date | Helpful count |\n|---|---|---|---|---|\n" + + "| The Dark Knight | Alice | 9/10 | 2020-02-03 | 42 |\n" + + "| The Dark Knight | DustBowlPilot | 7/10 | 2021-03-04 | 18 |") + self.check(22, "| Film | Year | Headline | Author | Review score | Review date | Helpful count |\n|---|---|---|---|---|---|---|\n" + + "| The Dark Knight | 2008 | Same words | Alice | 9/10 | 2020-02-03 | 42 |\n" + + "| The Dark Knight | 2008 | Same words | DustBowlPilot | 7/10 | 2021-03-04 | 18 |") + + def test_closed_bilingual_fields_scores_dates_and_runtime(self): + answer21 = ANSWER21.replace("Account", "账号").replace("Film", "电影").replace("Personal rating", "个人评分").replace("| Director |", "| 导演 |").replace("Runtime", "片长").replace("minutes", "分钟") + self.check(21, answer21) + self.check(21, "Alice账号:\nThe Godfather:个人评分9分,导演Coppola,片长2小时55分钟。\nInception:个人评分10分,导演Nolan,片长148分钟。\nBob账号:\nThe Dark Knight:个人评分10分,导演Nolan,片长152分钟。\nSynthetic Cutoff Echo:个人评分9分,导演Synthetic Director,片长100分钟。") + answer22 = ANSWER22.replace("repeated headline:", "重复标题:").replace("Author", "作者").replace("Review score", "影评评分").replace("Review date", "评论日期").replace("Helpful count", "有用数") + self.check(22, answer22) + self.check(22, "The Dark Knight (2008),重复标题:Same words。\nAlice:影评评分9分(满分10分);日期2020年2月3日;有用数42。\nDustBowlPilot:影评评分7分(满分10分);日期2021年3月4日;有用数18。") + self.rejects(22, answer22.replace("9/10", "8/10")) + + def test_noop_answer_only_foreign_pages_and_extra_business_changes_fail(self): + for number in (21, 22): + with self.subTest(number=number): + self.rejects(number, answer="", steps=[self.step("/")]) + self.rejects(number, steps=[]) + steps = self.steps(number) + for step in steps: + step["url"] = step["url"].replace(self.origin, "https://www.imdb.com") + if "url_after" in step: + step["url_after"] = step["url_after"].replace(self.origin, "https://www.imdb.com") + self.rejects(number, steps=steps) + self.change("UPDATE reviews SET helpful_count=helpful_count+1", after_only=True) + self.rejects(21) + self.rejects(22) + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/imdb/tests/test_expansion_catalog_reads.py b/sites/imdb/tests/test_expansion_catalog_reads.py new file mode 100644 index 00000000..ed5fe0e8 --- /dev/null +++ b/sites/imdb/tests/test_expansion_catalog_reads.py @@ -0,0 +1,353 @@ +"""Constructed offline cases only; none of these are genuine UI runs.""" + +import importlib +import json +from pathlib import Path +import shutil +import sqlite3 +import sys +import tempfile +import unittest + +SITE = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SITE / "verify")) +grader = importlib.import_module("expansion_catalog_reads") +RunEvidence = importlib.import_module("verify_lib").RunEvidence +VerificationError = importlib.import_module("verify_lib").VerificationError + +SCHEMA = """ +CREATE TABLE titles (id INTEGER PRIMARY KEY, tt_id TEXT, title_type TEXT, + primary_title TEXT, original_title TEXT DEFAULT '', year INTEGER, + runtime_min INTEGER, mpaa_rating TEXT, rating_avg REAL, budget INTEGER, + box_office_opening INTEGER, box_office_us INTEGER, box_office_world INTEGER, + release_date TEXT); +CREATE TABLE persons (id INTEGER PRIMARY KEY, nm_id TEXT, name TEXT); +CREATE TABLE credits (id INTEGER PRIMARY KEY, title_id INTEGER, person_id INTEGER, + role TEXT, billing_order INTEGER); +CREATE TABLE genres (id INTEGER PRIMARY KEY, slug TEXT); +CREATE TABLE title_genre (title_id INTEGER, genre_id INTEGER); +CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT); +CREATE TABLE reviews (id INTEGER PRIMARY KEY, body TEXT); +CREATE TABLE user_ratings (id INTEGER PRIMARY KEY, rating INTEGER); +CREATE TABLE watchlist_items (id INTEGER PRIMARY KEY, title_id INTEGER); +""" + +ANSWERS = { + 18: ("| Film | IMDb rating | Runtime | MPAA |\n|---|---|---|---|\n" + "| Amber Run | 8.5 | 90 minutes | G |\n" + "| Bronze Tide | 8.5 | 100 minutes | PG |\n" + "| Cedar Light | 8.5 | 95 minutes | G |\n\n" + "Best pair: Amber Run + Bronze Tide; combined runtime 190 minutes; summed rating 17.0.\n" + "Best pair: Cedar Light + Amber Run; combined runtime 185 minutes; summed rating 17.0."), + 19: ("| Film | Budget | Opening weekend US & Canada | Percentage |\n|---|---|---|---|\n" + "| Amber Run | $70.0M | $30.6M | 43.7% |\n" + "| Bronze Tide | $52.0M | $30.1M | 57.9% |\n\nWinner: Bronze Tide (57.9%)."), + 20: ("| Film | Gross US & Canada | Gross worldwide | Remainder share |\n|---|---|---|---|\n" + "| Amber Run | $30.0M | $100.0M | 70.0% |\n" + "| Bronze Tide | $120.0M | $200.0M | 40.0% |\n" + "| Cedar Light | $150.0M | $300.0M | 50.0% |\n\nWinner: Amber Run (70.0%)."), + 23: ("| Film | Release date |\n|---|---|\n" + "| Amber Run | 2000-01-01 |\n| Bronze Tide | 2000-01-05 |\n" + "| Cedar Light | 2000-01-09 |\n\n" + "Closest pair: Amber Run to Bronze Tide — 4 calendar days.\n" + "Closest pair: Bronze Tide to Cedar Light — 4 days."), +} + + +class CatalogReadTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.folder = Path(self.temp.name) + self.origin = "http://catalog-read.localhost:48021" + with sqlite3.connect(self.folder / "before.db") as db: + db.executescript(SCHEMA) + db.executemany("INSERT INTO titles VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)", [ + (1, "tt9000001", "movie", "Amber Run", "", 2000, 90, "G", 8.5, + 70000000, 30576104, 29960000, 100040000, "2000-01-01"), + (2, "tt9000002", "movie", "Bronze Tide", "", 2000, 100, "PG", 8.5, + 52000000, 30053627, 120000000, 200000000, "2000-01-05"), + (3, "tt9000003", "movie", "Cedar Light", "", 2000, 95, "G", 8.5, + 40000000, 10000000, 150000000, 300000000, "2000-01-09"), + (4, "tt9000004", "movie", "Distant Gate", "", 2000, 80, "Not Rated", 9.5, + 10000000, 9000000, 1000000, 100000000, "2000-01-10"), + (5, "tt9000005", "movie", "Evening Road", "", 2012, 70, "G", 9.9, + 10000000, 9000000, 1000000, 100000000, "2012-01-01"), + (6, "tt9000006", "tvSeries", "Forest Signal", "", 2001, 30, "G", 9.9, + 10000000, 9000000, 1000000, 100000000, "2001-01-01"), + ]) + db.executemany("INSERT INTO persons VALUES (?,?,?)", [ + (1, "nm0000229", "Steven Spielberg"), + (2, "nm0000158", "Tom Hanks"), + (3, "nm0000138", "Leonardo DiCaprio"), + ]) + credits = [(1, 1, "director"), (2, 1, "director"), (3, 1, "writer"), + (1, 2, "actor"), (2, 2, "actor"), (3, 2, "actor"), + (4, 2, "writer"), (6, 2, "actor"), + (1, 3, "actor"), (2, 3, "actor"), (3, 3, "actor"), + (4, 3, "director"), (6, 3, "actor")] + db.executemany("INSERT INTO credits VALUES (?,?,?,?,?)", + [(i, title, person, role, i) for i, (title, person, role) in enumerate(credits, 1)]) + db.execute("INSERT INTO genres VALUES (1,'animation')") + db.executemany("INSERT INTO title_genre VALUES (?,1)", [(i,) for i in range(1, 7)]) + db.execute("INSERT INTO users VALUES (1,'Synthetic account')") + db.execute("INSERT INTO reviews VALUES (1,'Synthetic content')") + shutil.copyfile(self.folder / "before.db", self.folder / "after.db") + + def mutate(self, sql, params=(), both=True): + for filename in (["before.db", "after.db"] if both else ["after.db"]): + with sqlite3.connect(self.folder / filename) as db: + db.execute(sql, params) + + def check(self, task, answer=None, paths=None): + paths = ["/title/tt9000001", "/title/tt9000002", "/title/tt9000003"] if paths is None else paths + trajectory = {"task_id": f"IMDb--{task}", "start_url": self.origin + "/", + "final_answer": ANSWERS[task] if answer is None else answer, + "steps": [{"step": i, "url": self.origin + "/", + "url_after": path if path.startswith("http") else self.origin + path, + "status": "completed", "action": "navigate"} + for i, path in enumerate(paths)]} + (self.folder / "trajectory.json").write_text(json.dumps(trajectory)) + with RunEvidence(self.folder, f"IMDb--{task}") as run: + return grader.check_catalog_read_task(task, run) + + def reject(self, task, answer=None, paths=None): + with self.assertRaises(VerificationError): + self.check(task, answer, paths) + + def test_positive_tables_all_tasks(self): + for task in ANSWERS: + with self.subTest(task=task): + self.assertTrue(self.check(task)) + + def test_no_answer_noop_no_navigation_and_foreign_sources(self): + for task in ANSWERS: + with self.subTest(task=task): + self.reject(task, "") + self.reject(task, "Done.") + self.reject(task, paths=[]) + self.reject(task, paths=["http://foreign.example/title/tt9000001", + "http://foreign.example/title/tt9000002", + "http://foreign.example/title/tt9000003"]) + + def test_wrong_title_binding_and_partial_rows(self): + for task in ANSWERS: + with self.subTest(task=task): + self.reject(task, ANSWERS[task].replace("| Amber Run |", "| Distant Gate |", 1)) + self.reject(task, "\n".join(line for line in ANSWERS[task].splitlines() + if not line.startswith("| Bronze Tide |"))) + + def test_any_business_table_mutation_rejected(self): + for sql in ("UPDATE reviews SET body='Changed'", "UPDATE users SET name='Changed'", + "UPDATE titles SET year=2005 WHERE id=1", + "INSERT INTO watchlist_items VALUES (1,1)", + "INSERT INTO user_ratings VALUES (1,10)"): + shutil.copyfile(self.folder / "before.db", self.folder / "after.db") + self.mutate(sql, both=False) + for task in ANSWERS: + with self.subTest(sql=sql, task=task): + self.reject(task) + + def test_navigation_order_and_equivalent_credit_sources(self): + for task in ANSWERS: + self.assertTrue(self.check(task, paths=["/name/nm0000158", "/name/nm0000138", + "/title/tt9000003/", "/title/tt9000002/", + "/title/tt9000001/"])) + + def test_credit_outside_first_fifteen_needs_profile_or_fullcredits(self): + for i in range(16): + self.mutate("INSERT INTO credits VALUES (?,?,?,?,?)", (100+i, 1, 100+i, "actor", -i-1)) + self.reject(19) + self.assertTrue(self.check(19, paths=["/title/tt9000001", "/title/tt9000002", + "/title/tt9000001/fullcredits"])) + + def test_19_equivalent_units_and_prose(self): + answer = ("Amber Run: budget USD 70 million; opening weekend $30,600,000; 43.7 percent.\n" + "Bronze Tide: budget $52 million; opening $30.1 million; 57.9%.\n" + "Bronze Tide has the largest percentage, while Amber Run is lower.") + self.assertTrue(self.check(19, answer)) + + def test_19_20_chinese_maximum_cue(self): + for task, winner in ((19, "Bronze Tide"), (20, "Amber Run")): + answer = ANSWERS[task].replace(f"Winner: {winner}", f"最高为 {winner}") + with self.subTest(task=task): + self.assertTrue(self.check(task, answer)) + + def test_19_displayed_precision_required(self): + self.reject(19, ANSWERS[19].replace("30.6M", "30,576,104")) + self.reject(19, ANSWERS[19].replace("43.7%", "43.6%")) + + def test_19_swapped_fields_and_amounts(self): + self.reject(19, ANSWERS[19].replace("$70.0M | $30.6M", "$30.6M | $70.0M")) + self.reject(19, ANSWERS[19].replace("$30.6M", "$30.1M").replace("43.7%", "57.9%")) + self.reject(19, ANSWERS[19].replace("Opening weekend US & Canada", "Gross worldwide")) + + def test_19_role_change_derives_new_snapshot_intersection(self): + self.mutate("UPDATE credits SET role='writer' WHERE title_id=2 AND person_id=1") + self.reject(19) + self.assertTrue(self.check(19, "Amber Run: budget $70M; opening $30.6M; 43.7%.\nWinner: Amber Run.")) + + def test_19_all_maximum_ties(self): + self.mutate("UPDATE titles SET budget=70000000,box_office_opening=30576104 WHERE id=2") + answer = ANSWERS[19].replace("52.0M", "70.0M").replace("30.1M", "30.6M").replace("57.9%", "43.7%") + self.reject(19, answer) + self.assertTrue(self.check(19, answer.replace("Winner: Bronze Tide (43.7%).", "Winners: Amber Run and Bronze Tide (43.7%)."))) + + def test_19_false_or_unknown_extra_winner(self): + self.reject(19, ANSWERS[19].replace("Winner: Bronze Tide", "Winners: Bronze Tide and Amber Run")) + self.reject(19, ANSWERS[19].replace("Winner: Bronze Tide", "Winners: Bronze Tide and Imaginary Feature")) + + def test_20_false_winner_and_wrong_percent(self): + self.reject(20, ANSWERS[20].replace("Winner: Amber Run", "Winner: Cedar Light")) + self.reject(20, ANSWERS[20].replace("70.0%", "70.1%")) + + def test_20_reversed_fields_and_role_only_extra(self): + self.reject(20, ANSWERS[20].replace("Gross US & Canada", "X").replace("Gross worldwide", "Gross US & Canada").replace("X", "Gross worldwide")) + self.reject(20, ANSWERS[20] + "\nDistant Gate: domestic $1M; worldwide $100M; 99.0%.") + self.assertTrue(self.check(20, ANSWERS[20] + "\nDistant Gate is excluded: it is a Director credit, not Actor.")) + + def test_20_snapshot_gross_change_changes_truth(self): + self.mutate("UPDATE titles SET box_office_us=60000000 WHERE id=1") + self.reject(20) + answer = ANSWERS[20].replace("$30.0M", "$60.0M").replace("70.0%", "40.0%") + answer = answer.replace("Winner: Amber Run (40.0%).", "Winner: Cedar Light (50.0%).") + self.assertTrue(self.check(20, answer)) + + def test_20_tied_winners_are_complete(self): + self.mutate("UPDATE titles SET box_office_us=90000000 WHERE id=3") + answer = ANSWERS[20].replace("$150.0M", "$90.0M").replace("50.0%", "70.0%") + self.reject(20, answer) + self.assertTrue(self.check(20, answer.replace("Winner: Amber Run (70.0%).", "Winners: Amber Run and Cedar Light (70.0%)."))) + + def test_unknown_extra_table_and_prose_rows(self): + self.reject(20, ANSWERS[20].replace("| Bronze Tide |", "| Imaginary Feature |")) + self.reject(20, ANSWERS[20] + "\nImaginary Feature: domestic $3M; worldwide $100M; 97.0%.") + + def test_prefix_titles_and_middle_dot_aliases(self): + self.mutate("UPDATE titles SET primary_title='Toy Story' WHERE id=1") + self.mutate("UPDATE titles SET primary_title='Toy Story 3' WHERE id=2") + self.mutate("UPDATE titles SET primary_title='WALL·E' WHERE id=3") + for task in ANSWERS: + answer = ANSWERS[task].replace("Amber Run", "Toy Story").replace("Bronze Tide", "Toy Story 3").replace("Cedar Light", "WALL-E") + self.assertTrue(self.check(task, answer)) + + def test_18_pair_constraints_ties_and_bound_fields(self): + for before, after in [("190 minutes", "191 minutes"), ("17.0", "17.1"), + ("90 minutes", "95 minutes"), ("| G |", "| PG-13 |")]: + self.reject(18, ANSWERS[18].replace(before, after, 1)) + self.reject(18, ANSWERS[18].split("\nBest pair: Cedar")[0]) + self.reject(18, ANSWERS[18] + "\nBest pair: Bronze Tide + Cedar Light; total runtime 195 minutes; summed rating 17.0.") + + def test_18_hour_units_and_reversed_pairs(self): + answer = ANSWERS[18].replace("| 90 minutes |", "| 1 hour 30 minutes |").replace("| 100 minutes |", "| 1h 40m |").replace("| 95 minutes |", "| 1 hour 35 minutes |") + answer = answer.replace("190 minutes", "3 hours 10 minutes").replace("185 minutes", "3h 5m") + self.assertTrue(self.check(18, answer)) + + def test_18_mpaa_and_year_are_derived(self): + self.mutate("UPDATE titles SET mpaa_rating='G' WHERE id=4") + self.reject(18) + + def test_unknown_and_self_pairs_are_rejected(self): + self.reject(18, ANSWERS[18] + "\nBest pair: Amber Run and Imaginary Feature; combined runtime 190 minutes; summed rating 17.0.") + self.reject(18, ANSWERS[18] + "\nBest pair: Amber Run and Amber Run; combined runtime 180 minutes; summed rating 17.0.") + self.reject(23, ANSWERS[23] + "\nClosest pair: Imaginary Feature and Cedar Light, 4 days.") + + def test_18_prose_film_facts_and_pair_table(self): + answer = ("Amber Run: IMDb rating 8.5; runtime 90 minutes; MPAA G.\n" + "Bronze Tide: IMDb rating 8.5; runtime 100 minutes; MPAA PG.\n" + "Cedar Light: IMDb rating 8.5; runtime 95 minutes; MPAA G.\n\n" + "| First film | Second film | Combined runtime (min) | Summed rating |\n|---|---|---|---|\n" + "| Amber Run | Bronze Tide | 190 | 17 |\n| Amber Run | Cedar Light | 185 | 17 |") + self.assertTrue(self.check(18, answer)) + + def test_18_single_feature_table_then_separate_totals(self): + self.mutate("UPDATE titles SET rating_avg=8.4 WHERE id=3") + table = ("| Film | IMDb rating | Runtime | MPAA |\n|---|---|---|---|\n" + "| Amber Run | 8.5 | 90 minutes | G |\n" + "| Bronze Tide | 8.5 | 100 minutes | PG |\n") + totals = "Combined runtime: 190 minutes.\nSummed rating: 17.0." + for heading in ("The best double feature is:\n", "Best pair: Amber Run + Bronze Tide\n"): + for separator in ("", "\n"): + with self.subTest(heading=heading, separator=separator): + answer = heading + table + separator + totals + self.assertTrue(self.check(18, answer)) + self.reject(18, answer.replace("190 minutes", "189 minutes")) + self.reject(18, answer.replace("17.0", "17.1")) + + def test_18_multi_pair_unattributed_totals_remain_ambiguous(self): + table = ANSWERS[18].split("\n\nBest pair:")[0] + answer = ("The best double features are:\n" + table + "\n\n" + "Combined runtime: 190 minutes.\nSummed rating: 17.0.\n" + "Combined runtime: 185 minutes.\nSummed rating: 17.0.") + self.reject(18, answer) + answer = ("Best pair: Amber Run + Bronze Tide\nBest pair: Amber Run + Cedar Light\n" + table + + "\nCombined runtime: 190 minutes.\nCombined runtime: 185 minutes.\nSummed rating: 17.0.") + self.reject(18, answer) + + def test_18_chinese_equivalent_fields_totals_and_exclusion(self): + self.mutate("UPDATE titles SET rating_avg=8.4 WHERE id=3") + answer = ("最优组合只有一组:Amber Run(2000),IMDb 8.5,90分钟,MPAA G;" + "Bronze Tide(2000),IMDb 8.5,100分钟,MPAA PG。合计190分钟,评分合计17.0。" + "Distant Gate虽为9.5,但页面分级为Not Rated,不符合G/PG;因此没有并列最优组合。") + self.assertTrue(self.check(18, answer)) + for before, after in (("合计190", "合计189"), ("评分合计17.0", "评分合计16.9"), + ("IMDb 8.5,90分钟", "IMDb 8.4,90分钟"), + ("不符合G/PG", "符合G/PG")): + self.reject(18, answer.replace(before, after, 1)) + self.reject(18, answer.replace("90分钟", "TEMP").replace("100分钟", "90分钟").replace("TEMP", "100分钟")) + + def test_19_winner_flag_table(self): + answer = ("| Film | Budget | Opening | Ratio (%) | Winner |\n|---|---|---|---|---|\n" + "| Bronze Tide | USD 52000000 | USD 30100000 | 57.9 | yes |\n" + "| Amber Run | USD 70000000 | USD 30600000 | 43.7 | no |") + self.assertTrue(self.check(19, answer)) + + def test_20_prose_and_short_domestic_column_alias(self): + answer = ("Cedar Light: domestic gross $150 million; worldwide gross $300 million; 50 percent.\n" + "Amber Run: domestic gross USD 30000000; worldwide gross USD 100000000; 70 percent.\n" + "Bronze Tide: domestic gross $120M; worldwide $200M; 40 percent.\n" + "Amber Run has the largest percentage.") + self.assertTrue(self.check(20, answer)) + self.assertTrue(self.check(20, ANSWERS[20].replace("Gross US & Canada", "US & Canada"))) + + def test_23_written_dates_and_pair_table(self): + answer = ("Amber Run: January 1st, 2000.\nBronze Tide: 5 January 2000.\n" + "Cedar Light: Jan 9, 2000.\n\n" + "| First film | Second film | Gap (days) |\n|---|---|---|\n" + "| Bronze Tide | Amber Run | 4 |\n| Cedar Light | Bronze Tide | 4 |") + self.assertTrue(self.check(23, answer)) + + def test_23_partial_ties_false_pair_and_wrong_gap(self): + self.reject(23, ANSWERS[23].split("\nClosest pair: Bronze")[0]) + self.reject(23, ANSWERS[23] + "\nClosest pair: Amber Run to Cedar Light - 8 days.") + self.reject(23, ANSWERS[23].replace("4 calendar days", "5 calendar days")) + + def test_23_date_swap_and_nonchronological_table(self): + self.reject(23, ANSWERS[23].replace("2000-01-01", "2000-01-02")) + self.reject(23, ANSWERS[23].replace("| Amber Run | 2000-01-01 |\n| Bronze Tide | 2000-01-05 |", + "| Bronze Tide | 2000-01-05 |\n| Amber Run | 2000-01-01 |")) + + def test_23_dates_before_titles_and_repeated_pair_dates(self): + answer = ("| Release date | Film |\n|---|---|\n| January 1, 2000 | Amber Run |\n" + "| January 5, 2000 | Bronze Tide |\n| January 9, 2000 | Cedar Light |\n\n" + "Closest pair: Amber Run (2000-01-01) to Bronze Tide (2000-01-05), 4 days.\n" + "Closest pair: Bronze Tide (2000-01-05) to Cedar Light (2000-01-09), 4 days.") + self.assertTrue(self.check(23, answer)) + self.reject(23, answer.replace("Amber Run (2000-01-01) to Bronze Tide (2000-01-05)", + "Amber Run (2000-01-05) to Bronze Tide (2000-01-01)")) + + def test_23_single_paragraph_timeline(self): + answer = ("Timeline: Amber Run: January 1, 2000; Bronze Tide: January 5, 2000; Cedar Light: January 9, 2000.\n" + "Closest pair: Amber Run to Bronze Tide, 4 days.\nClosest pair: Bronze Tide to Cedar Light, 4 days.") + self.assertTrue(self.check(23, answer)) + + def test_23_same_date_order_is_not_an_id_tiebreaker(self): + self.mutate("UPDATE titles SET release_date='2000-01-01' WHERE id IN (2,3)") + answer = ("Cedar Light: January 1, 2000.\nAmber Run: January 1, 2000.\nBronze Tide: January 1, 2000.\n" + "Closest pair: Cedar Light and Amber Run, 0 days.\nClosest pair: Amber Run and Bronze Tide, 0 days.") + self.assertTrue(self.check(23, answer)) + self.reject(23, answer + "\nClosest pair: Cedar Light and Bronze Tide, 0 days.") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/imdb/tests/test_expansion_state_tasks.py b/sites/imdb/tests/test_expansion_state_tasks.py new file mode 100644 index 00000000..e426e16d --- /dev/null +++ b/sites/imdb/tests/test_expansion_state_tasks.py @@ -0,0 +1,470 @@ +"""Synthetic offline fixtures only; these are not recorded browser runs.""" +import hashlib +import importlib +import json +from pathlib import Path +import shutil +import sqlite3 +import sys +import tempfile +import unittest + +SITE = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SITE / "verify")) +checks = importlib.import_module("expansion_state_tasks") +from verify_lib import RunEvidence, VerificationError + +PASSWORD = "TestPass123!" +EMAILS = {1: "alice.j@test.com", 2: "bob.c@test.com", 4: "david.k@test.com", + 9: "weekend.viewer@test.com"} +HASH = "pbkdf2:sha256:1000$salt$" + hashlib.pbkdf2_hmac( + "sha256", PASSWORD.encode(), b"salt", 1000).hex() +SCHEMA = """ +CREATE TABLE users(id INTEGER PRIMARY KEY,email TEXT,name TEXT,password_hash TEXT,created_at TEXT); +CREATE TABLE titles(id INTEGER PRIMARY KEY,tt_id TEXT,primary_title TEXT,original_title TEXT, + title_type TEXT,year INTEGER,runtime_min INTEGER,rating_avg REAL); +CREATE TABLE genres(id INTEGER PRIMARY KEY,name TEXT,slug TEXT); +CREATE TABLE title_genre(title_id INTEGER,genre_id INTEGER); +CREATE TABLE watchlist_items(id INTEGER PRIMARY KEY,user_id INTEGER,title_id INTEGER,added_at TEXT); +CREATE TABLE user_ratings(id INTEGER PRIMARY KEY,user_id INTEGER,title_id INTEGER,rating INTEGER,created_at TEXT); +CREATE TABLE reviews(id INTEGER PRIMARY KEY,title_id INTEGER,user_id INTEGER,rating INTEGER, + headline TEXT,body TEXT,helpful_count INTEGER,created_at TEXT,is_seed INTEGER); +CREATE TABLE persons(id INTEGER PRIMARY KEY,name TEXT); +CREATE TABLE credits(id INTEGER PRIMARY KEY,title_id INTEGER,person_id INTEGER,role TEXT); +CREATE TABLE news_items(id INTEGER PRIMARY KEY,headline TEXT); +""" +TITLES = [(1, "Short Alpha", "movie", 1980, 100, 8.4), + (2, "Short Beta", "movie", 1985, 120, 8.0), + (3, "Over Limit", "movie", 1981, 121, 9.0), + (4, "Already Saved", "movie", 1982, 105, 8.8), + (5, "Ten Star Show", "tvSeries", 1970, 90, 9.5), + (6, "Outside Years", "movie", 1990, 99, 8.9), + (7, "Review Film", "movie", 2014, 169, 8.7), + (8, "Other Unrated", "movie", 2019, 132, 8.5), + (10, "Weak Signal", "movie", 1975, 95, 7.9)] +ANSWERS = { + 24: "Added Short Alpha (1980), 100 minutes, and Short Beta (1985), 120 minutes. Both are in David's Watchlist.", + 25: "Added Short Alpha (1980). Already Saved (1982) was already saved in David's Watchlist and remains there.", + 26: "Created Weekend Viewer. Review Film (2014) and Other Unrated (2019) remain in the new Watchlist after signing back in.", + 27: 'Review Film (2014): Alice’s review "An earned score" gives 7/10. Saved a personal rating of 7/10 and confirmed My ratings.', +} + + +class SyntheticExpansionStateTests(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.directory = Path(self.tmp.name) + self.origin = "http://synthetic-new-state.localhost:54321" + with sqlite3.connect(self.directory / "before.db") as db: + db.executescript(SCHEMA) + db.executemany("INSERT INTO users VALUES(?,?,?,?,?)", [ + (u, EMAILS[u], {1: "Alice", 2: "Bob", 4: "David"}[u], HASH, "2024-01-01") + for u in (1, 2, 4)]) + db.executemany("INSERT INTO titles VALUES(?,?,?,?,?,?,?,?)", [ + (i, f"tt{i:07}", name, "", kind, year, runtime, rating) + for i, name, kind, year, runtime, rating in TITLES]) + db.execute("INSERT INTO genres VALUES(1,'Sci-Fi','sci-fi')") + db.executemany("INSERT INTO title_genre VALUES(?,1)", [(i,) for i in (1, 2, 3, 4, 5, 6, 10)]) + db.executemany("INSERT INTO watchlist_items VALUES(?,?,?,?)", [ + (1, 4, 4, "2024-01-01"), (2, 1, 7, "2024-01-01"), + (3, 1, 8, "2024-01-01"), (4, 1, 4, "2024-01-01")]) + db.executemany("INSERT INTO user_ratings VALUES(?,?,?,?,?)", [ + (1, 2, 1, 10, "2024-01-01"), (2, 2, 4, 10, "2024-01-01"), + (3, 2, 5, 10, "2024-01-01"), (4, 2, 2, 9, "2024-01-01"), + (5, 1, 4, 9, "2024-01-01")]) + db.executemany("INSERT INTO reviews VALUES(?,?,?,?,?,?,?,?,?)", [ + (1, 7, 1, 7, "An earned score", "Original", 42, "2024-01-01", 0), + (2, 7, 2, 9, "An earned score", "Distractor", 99, "2024-01-01", 1), + (3, 8, 2, 8, "Other author", "Other", 10, "2024-01-01", 1)]) + db.execute("INSERT INTO news_items VALUES(1,'Keep unchanged')") + self.reset_after() + + def reset_after(self): + shutil.copyfile(self.directory / "before.db", self.directory / "after.db") + + def change(self, sql, parameters=(), both=False): + for name in (("before.db", "after.db") if both else ("after.db",)): + with sqlite3.connect(self.directory / name) as db: + db.execute(sql, parameters) + + def apply_delta(self, number): + uid, titles = {24: (4, [1, 2]), 25: (4, [1]), 26: (9, [7, 8])}.get(number, (None, [])) + if number == 26: + self.change("INSERT INTO users VALUES(9,?,?,?,?)", (EMAILS[9], "Weekend Viewer", HASH, "2026-01-01")) + for i, tid in enumerate(titles): + self.change("INSERT INTO watchlist_items VALUES(?,?,?,?)", (20 + i, uid, tid, "2026-01-01")) + if number == 27: + self.change("INSERT INTO user_ratings VALUES(20,1,7,7,'2026-01-01')") + + def step(self, path, action="observe", params=None, after=None, native=False): + row = {"url": self.origin + path, "action": action, "params": params or {}, + "action_result": {"success": None if native else True}} + if after is not None: + row["url_after"] = self.origin + after + return row + + def login(self, uid, native=False): + return [self.step("/login", "fill", {"label": "Email", "value": EMAILS[uid]}), + self.step("/login", "fill", {"label": "Password", "value": PASSWORD}), + self.step("/login", "click", {"role": "button", "name": "Sign in"}, "/", native), + self.step("/")] + + def logout(self, native=False, account=False): + return [self.step("/account" if account else "/", "click", + {"role": "button", "name": "Sign out", "method": "POST"}, "/", native), + self.step("/")] + + def add(self, tid, chart=False, native=False): + path = "/chart/top" if chart else f"/title/tt{tid:07}" + params = ({"selector": "li", "has_link": dict((r[0], r[1]) for r in TITLES)[tid], + "child": {"role": "button", "name": "+ Watchlist"}} if chart else + {"role": "button", "name": "Add to Watchlist"}) + return [self.step(path, "click", params, path, native), self.step(path)] + + def steps(self, number, chart=False, native=False): + if number == 24: + out = self.login(4, native) + [self.step("/list/watchlist"), + self.step("/search/title?title_type=movie&genre=sci-fi&year_from=1960&year_to=1989&rating_min=8.0")] + for tid in (1, 2): out += self.add(tid, chart, native) + return out + [self.step("/list/watchlist")] + if number == 25: + return (self.login(2, native) + [self.step("/list/ratings"), self.step("/title/tt0000001"), + self.step("/title/tt0000004"), self.step("/title/tt0000005")] + self.logout(native) + + self.login(4, native) + [self.step("/list/watchlist")] + self.add(1, chart, native) + + [self.step("/list/watchlist")]) + if number == 26: + out = self.login(1, native) + [self.step("/list/watchlist"), self.step("/list/ratings")] + self.logout(native) + for field, value in {"name": "Weekend Viewer", "email": EMAILS[9], "password": PASSWORD}.items(): + out.append(self.step("/register", "fill", {"label": field, "value": value})) + out += [self.step("/register", "click", {"role": "button", "name": "Create account"}, "/", native), self.step("/")] + for tid in (7, 8): out += self.add(tid, chart, native) + return out + self.logout(native, account=True) + self.login(9, native) + [self.step("/list/watchlist")] + return (self.login(1, native) + [self.step("/list/watchlist"), self.step("/list/ratings"), + self.step("/title/tt0000007/reviews?sort=recent"), + self.step("/title/tt0000007", "select", {"label": "rating", "value": "7"}), + self.step("/title/tt0000007", "click", {"role": "button", "name": "Rate"}, "/title/tt0000007", native), + self.step("/list/ratings")]) + + def check(self, number, answer=None, steps=None): + data = {"task_id": f"IMDb--{number}", "task": "Synthetic contract", "start_url": self.origin + "/", + "fixture_kind": "synthetic", "steps": self.steps(number) if steps is None else steps, + "final_answer": ANSWERS[number] if answer is None else answer} + (self.directory / "trajectory.json").write_text(json.dumps(data)) + with RunEvidence(self.directory, data["task_id"], expected_ques="Synthetic contract") as run: + return checks.check_expansion_state_task(number, run) + + def test_all_four_exact_changes(self): + for n in (24, 25, 26, 27): + with self.subTest(n=n): + self.reset_after(); self.apply_delta(n); self.assertTrue(self.check(n)) + + def test_native_unknown_outcomes_are_corroborated(self): + for n in (24, 25, 26, 27): + with self.subTest(n=n): + self.reset_after(); self.apply_delta(n); self.assertTrue(self.check(n, steps=self.steps(n, native=True))) + + def test_chart_child_add_controls(self): + for n in (24, 25, 26): + with self.subTest(n=n): + self.reset_after(); self.apply_delta(n); self.assertTrue(self.check(n, steps=self.steps(n, chart=True))) + + def test_noop_and_answer_only_fail(self): + for n in (24, 25, 26, 27): + with self.subTest(n=n): + with self.assertRaises(VerificationError): self.check(n) + self.apply_delta(n) + with self.assertRaises(VerificationError): self.check(n, steps=[self.step("/")]) + self.reset_after() + + def test_extra_write_or_schema_change_fails(self): + for sql in ("UPDATE news_items SET headline='Changed'", "CREATE INDEX extra ON users(email)"): + for n in (24, 25, 26, 27): + with self.subTest(n=n, sql=sql): + self.reset_after(); self.apply_delta(n); self.change(sql) + with self.assertRaises(VerificationError): self.check(n) + + def test_foreign_origin_rejected(self): + self.apply_delta(24); steps = self.steps(24) + for step in steps: + for field in ("url", "url_after"): + if field in step: step[field] = step[field].replace(self.origin, "http://other.localhost:54321") + with self.assertRaises(VerificationError): self.check(24, steps=steps) + + def test_wrong_filter_and_conflicting_duplicate_fail(self): + self.apply_delta(24) + for replacement in ("genre=drama", "genre=sci-fi&genre=drama", "genre=sci-fi&rating_min=9"): + steps = self.steps(24); steps[5]["url"] = steps[5]["url"].replace("genre=sci-fi", replacement) + with self.subTest(replacement=replacement), self.assertRaises(VerificationError): self.check(24, steps=steps) + + def test_filter_numeric_equivalence_and_no_sort_requirement(self): + self.apply_delta(24); steps = self.steps(24) + steps[5]["url"] += "&rating_min=8&sort=votes" + self.assertTrue(self.check(24, steps=steps)) + + def test_partial_or_wrong_target(self): + self.apply_delta(24); self.change("DELETE FROM watchlist_items WHERE title_id=2") + with self.assertRaises(VerificationError): self.check(24) + self.change("INSERT INTO watchlist_items VALUES(22,4,3,'2026-01-01')") + with self.assertRaises(VerificationError): self.check(24) + + def test_wrong_child_or_row_or_nested_metadata_rejected(self): + self.apply_delta(24) + for params in ({"role": "button", "name": "Remove from Watchlist"}, + {"selector": "li", "has_link": "Over Limit", "child": {"name": "Add to Watchlist"}}, + {"metadata": {"name": "Add to Watchlist"}}, + {"selector": "li", "has_link": "Short Alpha", "child": {"name": "Details"}}): + steps = self.steps(24, chart=True); steps[6]["params"] = params + with self.subTest(params=params), self.assertRaises(VerificationError): self.check(24, steps=steps) + + def test_failed_add_cannot_be_used(self): + self.apply_delta(24); steps = self.steps(24); steps[6]["action_result"]["success"] = False + with self.assertRaises(VerificationError): self.check(24, steps=steps) + + def test_wrong_year_runtime_and_partial_answer(self): + self.apply_delta(24) + for a in (ANSWERS[24].replace("1985", "1986"), ANSWERS[24].replace("120 minutes", "119 minutes"), + "Added Short Alpha (1980), 100 minutes.", ""): + with self.subTest(a=a), self.assertRaises(VerificationError): self.check(24, answer=a) + + def test_markdown_fields_and_reverse_order(self): + self.apply_delta(24) + a = "| Title | Year | Runtime |\n|---|---|---|\n| Short Beta |1985|120 minutes|\n|Short Alpha|1980|100 minutes|\nBoth added to David's Watchlist." + self.assertTrue(self.check(24, answer=a)) + + def test_chinese_runtime_units_with_existing_watchlist_summary(self): + self.apply_delta(24) + answer = ("已新增 Short Alpha(1980,100分钟)和 Short Beta(1985,120分钟)。" + "最终 David 的 Watchlist 包含这两部及原有 Already Saved。") + self.assertTrue(self.check(24, answer=answer)) + + def test_cross_account_needs_real_switch(self): + self.apply_delta(25); steps = self.steps(25) + steps = [s for s in steps if s["params"].get("name") != "Sign out"] + with self.assertRaises(VerificationError): self.check(25, steps=steps) + + def test_cross_account_existing_row_cannot_be_replaced(self): + self.apply_delta(25); self.change("UPDATE watchlist_items SET id=55 WHERE id=1") + with self.assertRaises(VerificationError): self.check(25) + + def test_already_saved_label_is_not_new_addition(self): + self.apply_delta(25) + with self.assertRaises(VerificationError): self.check(25, answer="Added Short Alpha (1980) and Already Saved (1982). Both were newly saved.") + self.assertTrue(self.check(25, answer="Already saved: Already Saved (1982); Added: Short Alpha (1980). Confirmed David's Watchlist.")) + + def test_registration_relogin_required(self): + self.apply_delta(26); steps = self.steps(26) + steps = steps[:-7] + [self.step("/list/watchlist")] + with self.assertRaises(VerificationError): self.check(26, steps=steps) + + def test_registration_wrong_password_hash(self): + self.apply_delta(26); self.change("UPDATE users SET password_hash=? WHERE id=9", (HASH.replace(HASH[-8:], "00000000"),)) + with self.assertRaises(VerificationError): self.check(26) + + def test_registration_wrong_owner(self): + self.apply_delta(26); self.change("UPDATE watchlist_items SET user_id=1 WHERE id>=20") + with self.assertRaises(VerificationError): self.check(26) + + def test_get_logout_does_not_clear_identity(self): + self.apply_delta(26); steps = self.steps(26) + for step in steps: + if step["params"].get("name") == "Sign out": + step["params"]["method"] = "GET"; step["action_result"]["status_code"] = 405 + with self.assertRaises(VerificationError): self.check(26, steps=steps) + + def test_bad_login_then_home_visit_is_not_authentication(self): + self.apply_delta(24); steps = self.steps(24) + steps[2]["url_after"] = self.origin + "/login" + with self.assertRaises(VerificationError): self.check(24, steps=steps) + + def test_no_password_submission_is_not_login(self): + self.apply_delta(24); steps = self.steps(24); del steps[1] + with self.assertRaises(VerificationError): self.check(24, steps=steps) + + def test_supplied_password_redaction_uses_snapshot_hash_and_successful_transition(self): + for marker in ("[SUPPLIED PASSWORD]", "[REDACTED]"): + for number in (24, 25, 26, 27): + with self.subTest(marker=marker, number=number): + self.reset_after(); self.apply_delta(number); steps = self.steps(number) + for step in steps: + if step["params"].get("label", "").lower() == "password": + step["params"]["value"] = marker + self.assertTrue(self.check(number, steps=steps)) + + def test_unknown_password_placeholder_is_not_authentication(self): + self.apply_delta(24); steps = self.steps(24) + steps[1]["params"]["value"] = "[MASKED]" + with self.assertRaises(VerificationError): + self.check(24, steps=steps) + + def test_review_author_derived_score_not_global_or_other_author(self): + self.apply_delta(27) + for score in (1, 9, 10): + self.change("UPDATE user_ratings SET rating=? WHERE id=20", (score,)) + with self.subTest(score=score), self.assertRaises(VerificationError): self.check(27) + + def test_review_featured_route_and_natural_headline(self): + self.apply_delta(27); steps = self.steps(27); steps[6]["url"] = self.origin + "/title/tt0000007" + self.assertTrue(self.check(27, steps=steps)) + + def test_review_headline_and_saved_score_contradictions(self): + self.apply_delta(27) + for answer in (ANSWERS[27].replace("An earned score", "An earned score extra"), + ANSWERS[27] + " Current personal rating is 9/10."): + with self.subTest(answer=answer), self.assertRaises(VerificationError): self.check(27, answer=answer) + + def test_review_source_page_is_required(self): + for rid in (10, 11, 12): + self.change("INSERT INTO reviews VALUES(?,7,2,9,'Other','Body',200,'2024-01-01',1)", (rid,), both=True) + self.apply_delta(27); steps = self.steps(27) + # Alice's review is outside the three featured rows; title alone no + # longer exposes the required authored review. + steps = [s for s in steps if "/reviews" not in s["url"]] + with self.assertRaises(VerificationError): self.check(27, steps=steps) + + def test_enter_and_index_only_controls(self): + for n in (24, 26, 27): + self.reset_after(); self.apply_delta(n); steps = self.steps(n, native=True) + for step in steps: + name = step["params"].get("name") + if name in {"Sign in", "Create account"}: + step["action"] = "press" + step["params"] = {"selector": "input[name=password]", "key": "Enter"} + elif name in {"Rate", "Add to Watchlist"}: + step["params"] = {"index": 8} + with self.subTest(n=n): self.assertTrue(self.check(n, steps=steps)) + + def test_generic_rating_submit_is_not_a_logout(self): + self.apply_delta(27); steps = self.steps(27) + steps[-2]["params"] = {"selector": "button[type=submit]"} + self.assertTrue(self.check(27, steps=steps)) + + def test_foreign_login_destination_not_repaired_by_later_home(self): + self.apply_delta(24); steps = self.steps(24) + steps[2]["url_after"] = "http://foreign.localhost:54321/" + with self.assertRaises(VerificationError): self.check(24, steps=steps) + + def test_year_filter_must_parse_like_app(self): + self.apply_delta(24); steps = self.steps(24) + steps[5]["url"] = steps[5]["url"].replace("year_from=1960", "year_from=1960.0") + with self.assertRaises(VerificationError): self.check(24, steps=steps) + + def test_type_filter_route_is_valid_only_for_matching_results(self): + self.apply_delta(25) + for valid in (True, False): + steps = self.steps(25) + steps[5:8] = [self.step("/search/title?title_type=movie" + ("" if valid else "&year_from=2000")), + self.step("/search/title?title_type=tvSeries")] + if valid: self.assertTrue(self.check(25, steps=steps)) + else: + with self.assertRaises(VerificationError): self.check(25, steps=steps) + + def test_source_and_destination_can_be_read_in_either_order(self): + self.apply_delta(25) + steps = self.login(4) + [self.step("/list/watchlist")] + self.logout() + steps += self.login(2) + [self.step("/list/ratings"), self.step("/title/tt0000001"), + self.step("/title/tt0000004"), self.step("/title/tt0000005")] + steps += self.logout() + self.login(4) + self.add(1) + [self.step("/list/watchlist")] + self.assertTrue(self.check(25, steps=steps)) + + def test_registration_may_reauthenticate_before_saving(self): + self.apply_delta(26); steps = self.steps(26) + registered = next(i for i,s in enumerate(steps) if s["params"].get("name") == "Create account") + steps[registered+2:registered+2] = self.logout() + self.login(9) + self.assertTrue(self.check(26, steps=steps)) + + def test_registration_form_values_must_match_submitted_account(self): + self.apply_delta(26); steps = self.steps(26) + for step in steps: + if step["url"].endswith("/register") and step["params"].get("label") == "name": + step["params"]["value"] = "Wrong name" + with self.assertRaises(VerificationError): self.check(26, steps=steps) + + def test_review_correct_global_rating_is_not_personal_score(self): + self.apply_delta(27) + self.assertTrue(self.check(27, answer=ANSWERS[27] + " IMDb rating is 8.7/10.")) + + def test_review_wrong_explicit_author_fails(self): + self.apply_delta(27) + with self.assertRaises(VerificationError): self.check(27, answer=ANSWERS[27].replace("Alice’s", "Bob’s")) + + def test_added_recommendation_cannot_be_reported_as_preexisting(self): + self.apply_delta(25) + with self.assertRaises(VerificationError): + self.check(25, answer="Short Alpha (1980) was already saved. Already Saved (1982) was also already saved.") + + def test_persistence_relogin_must_follow_all_additions(self): + self.apply_delta(26); steps = self.steps(26) + tail = steps[-7:-1] + steps = steps[:-7] + first_add = next(i for i, s in enumerate(steps) if s["params"].get("name") == "Add to Watchlist") + steps[first_add:first_add] = tail + steps.append(self.step("/list/watchlist")) + with self.assertRaises(VerificationError): self.check(26, steps=steps) + + def test_indexed_logout_requires_guarded_auth_page_transition(self): + self.apply_delta(26); steps = self.steps(26, native=True) + for step in steps: + if step["params"].get("name") == "Sign out": + step["params"] = {"index": 3, "method": "POST"} + self.assertTrue(self.check(26, steps=steps)) + for step in steps: + if step["params"].get("method") == "POST": step["params"]["method"] = "HEAD" + with self.assertRaises(VerificationError): self.check(26, steps=steps) + + def test_indexed_add_cannot_be_logout_if_next_route_is_title(self): + self.apply_delta(24); steps = self.steps(24, native=True) + for step in steps: + if step["params"].get("name") == "Add to Watchlist": step["params"] = {"index": 3} + self.assertTrue(self.check(24, steps=steps)) + + def test_indexed_logout_retains_explicit_ancestor_constraints(self): + self.apply_delta(26) + for target in ("http://foreign.localhost:54321/logout", "/title/tt0000007/watchlist"): + steps = self.steps(26, native=True) + for step in steps: + if step["params"].get("name") == "Sign out": + step["params"] = {"selector": f'form[action="{target}"]', "method": "POST", "child": {"index": 3}} + with self.subTest(target=target), self.assertRaises(VerificationError): self.check(26, steps=steps) + + def test_numeric_out_of_ten_score_and_contradiction(self): + self.apply_delta(27) + a = 'Review Film (2014): Alice’s review "An earned score". I saved 7 out of 10 as the personal rating and confirmed My ratings.' + self.assertTrue(self.check(27, answer=a)) + with self.assertRaises(VerificationError): self.check(27, answer=a + " The personal score is 9 out of 10.") + + def test_duplicate_add_claim_and_negated_claim(self): + self.apply_delta(25) + a = "Added Short Alpha (1980). Already Saved (1982) was already in David's Watchlist, and I added it again." + with self.assertRaises(VerificationError): self.check(25, answer=a) + self.assertTrue(self.check(25, answer=a.replace("I added it again", "I did not add it again"))) + + +class BoundedPasswordTests(unittest.TestCase): + def test_pbkdf2_correct_and_wrong(self): + self.assertTrue(checks.verify_password(HASH, PASSWORD)) + self.assertFalse(checks.verify_password(HASH, "wrong")) + + def test_scrypt_actual_werkzeug_shape(self): + digest = hashlib.scrypt(PASSWORD.encode(), salt=b"salt", n=32768, r=8, p=1, maxmem=64*1024*1024).hex() + self.assertTrue(checks.verify_password("scrypt:32768:8:1$salt$" + digest, PASSWORD)) + + def test_malformed_or_resource_exhausting_hashes_fail(self): + for value in ("plain$password", "pbkdf2:md5:1$salt$00", "pbkdf2:sha256:999999999$salt$00", + "scrypt:1073741824:8:1$salt$00", "scrypt:3:8:1$salt$00", HASH + "$extra"): + with self.subTest(value=value): self.assertFalse(checks.verify_password(value, PASSWORD)) + + def test_non_strings_and_invalid_unicode_fail_closed(self): + self.assertFalse(checks.verify_password({}, [])) + self.assertFalse(checks.verify_password(HASH, "\ud800")) + + def test_pbkdf2_sha512_preserves_password_whitespace(self): + password = " TestPass123! " + digest = hashlib.pbkdf2_hmac("sha512", password.encode(), b"salt", 1000).hex() + encoded = "pbkdf2:sha512:1000$salt$" + digest + self.assertTrue(checks.verify_password(encoded, password)) + self.assertFalse(checks.verify_password(encoded, password.strip())) + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/imdb/tests/test_homepage_seed.py b/sites/imdb/tests/test_homepage_seed.py new file mode 100644 index 00000000..0353e065 --- /dev/null +++ b/sites/imdb/tests/test_homepage_seed.py @@ -0,0 +1,226 @@ +"""Build-time snapshot import: catalog preservation and byte-stable re-import.""" +import importlib.util +import json +from pathlib import Path +import plistlib +import sqlite3 +import tempfile +import unittest +from copy import deepcopy + +spec = importlib.util.spec_from_file_location( + 'imdb_homepage_seed', Path(__file__).resolve().parents[1] / 'seed_homepage.py') +seed = importlib.util.module_from_spec(spec) +spec.loader.exec_module(seed) + +feature_spec = importlib.util.spec_from_file_location( + 'imdb_feature_seed', Path(__file__).resolve().parents[1] / 'seed_feature.py') +feature_seed = importlib.util.module_from_spec(feature_spec) +feature_spec.loader.exec_module(feature_seed) + + +class HomepageSeedTests(unittest.TestCase): + def test_feature_snapshot_import_is_sourced_local_and_idempotent(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + database = root / 'imdb.db' + static = root / 'static' + poster = static / 'images/home/source.jpg' + poster.parent.mkdir(parents=True) + poster.write_bytes(b'source-image') + snapshot = root / 'feature.json' + snapshot.write_text(json.dumps({ + 'feature_id': 'featured-today-1', + 'source_url': 'https://www.imdb.com/most-anticipated/this-month/', + 'observed_at': '2026-09-12T01:02:03Z', + 'page_title': 'Source feature', + 'page_subtitle': 'Source description', + 'hero_image_path': 'images/home/source.jpg', + 'hero_image_sha256': feature_seed.digest(b'source-image'), + 'items': [{ + 'position': 1, + 'source_id': 'tt1234567', + 'title': 'Source title', + 'source_url': 'https://www.imdb.com/title/tt1234567/', + 'poster_path': 'images/home/source.jpg', + 'poster_sha256': feature_seed.digest(b'source-image'), + 'release_context': 'In theaters September 12', + 'description': 'Observed description.', + 'featuring': ['Source Person'], + }], + })) + with sqlite3.connect(database) as con: + con.execute('CREATE TABLE titles (id INTEGER PRIMARY KEY, name TEXT)') + con.execute("INSERT INTO titles VALUES (1, 'Preserved')") + con.execute('''CREATE TABLE home_features ( + id VARCHAR(80) PRIMARY KEY, kind VARCHAR(30) NOT NULL, + position INTEGER NOT NULL, heading TEXT NOT NULL, subtitle TEXT, + image_path TEXT, poster_path TEXT, source_url TEXT NOT NULL, + captured_at VARCHAR(40) NOT NULL, payload JSON NOT NULL)''') + con.execute('''INSERT INTO home_features VALUES + ('featured-today-1','editorial',1,'Old heading','Old subtitle','','', + 'https://www.imdb.com/','2026-09-10','{"type":"list"}')''') + result = feature_seed.import_snapshot(snapshot, database, static) + self.assertEqual(result['items'], 1) + self.assertEqual(result['protected_tables_unchanged'], ['titles']) + with sqlite3.connect(database) as con: + self.assertEqual(con.execute('SELECT * FROM titles').fetchall(), [(1, 'Preserved')]) + payload = json.loads(con.execute( + "SELECT payload FROM home_features WHERE id='featured-today-1'").fetchone()[0]) + self.assertEqual(payload['items'][0]['title'], 'Source title') + self.assertNotIn('poster_source_url', payload['items'][0]) + before = database.read_bytes() + feature_seed.import_snapshot(snapshot, database, static) + self.assertEqual(database.read_bytes(), before) + + def test_feature_snapshot_rejects_external_or_missing_media(self): + base = { + 'feature_id': 'featured-today-1', + 'source_url': 'https://www.imdb.com/most-anticipated/this-month/', + 'observed_at': '2026-09-12T01:02:03Z', + 'page_title': 'Source feature', + 'page_subtitle': 'Source description', + 'hero_image_path': 'images/home/missing.jpg', + 'hero_image_sha256': '0' * 64, + 'items': [], + } + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + database = root / 'imdb.db' + with sqlite3.connect(database) as con: + con.execute('''CREATE TABLE home_features ( + id VARCHAR(80) PRIMARY KEY, kind VARCHAR(30) NOT NULL, + position INTEGER NOT NULL, heading TEXT NOT NULL, subtitle TEXT, + image_path TEXT, poster_path TEXT, source_url TEXT NOT NULL, + captured_at VARCHAR(40) NOT NULL, payload JSON NOT NULL)''') + con.execute('''INSERT INTO home_features VALUES + ('featured-today-1','editorial',1,'Old','','','', + 'https://www.imdb.com/','2026-09-10','{}')''') + snapshot = root / 'feature.json' + snapshot.write_text(json.dumps(base)) + with self.assertRaisesRegex(ValueError, 'media'): + feature_seed.import_snapshot(snapshot, database, root / 'static') + invalid = dict(base, source_url='https://example.com/not-imdb') + snapshot.write_text(json.dumps(invalid)) + with self.assertRaisesRegex(ValueError, 'IMDb'): + feature_seed.import_snapshot(snapshot, database, root / 'static') + + def test_starmeter_keeps_observed_rank_and_optional_fields(self): + props = {'pageData': {'chartNames': {'edges': [ + {'currentRank': 7, 'node': {'id': 'nm123', 'nameText': {'text': 'Source Person'}, + 'primaryImage': {'url': 'https://m.media-amazon.com/a.jpg'}, + 'professions': [{'profession': {'text': 'Actor'}}], + 'knownForV2': {'credits': [{'title': {'id': 'tt123', + 'titleText': {'text': 'Source Film'}}}]}}}, + {'currentRank': 2, 'node': {'id': 'nm456', 'nameText': {'text': 'Another Person'}}}, + ]}}} + entries = seed.starmeter_entries(props) + self.assertEqual([entry['rank'] for entry in entries], [7, 2]) + self.assertEqual(entries[0]['professions'], ['Actor']) + self.assertEqual(entries[0]['known_for'], ['Source Film']) + self.assertEqual(entries[1]['image'], '') + self.assertEqual(entries[1]['professions'], []) + self.assertNotIn('rank_change', entries[1], 'Do not fabricate chart movements') + invalid = deepcopy(props) + invalid['pageData']['chartNames']['edges'][1]['currentRank'] = 7 + with self.assertRaisesRegex(ValueError, 'rank'): + seed.starmeter_entries(invalid) + invalid = deepcopy(props) + del invalid['pageData']['chartNames']['edges'][0]['currentRank'] + with self.assertRaises(ValueError): + seed.starmeter_entries(invalid) + + def test_chart_only_archive_preserves_homepage_and_is_idempotent(self): + props = {'requestContext': {'timestamp': '2026-09-10T15:18:46Z'}, + 'pageData': {'chartNames': {'edges': [ + {'currentRank': 3, 'node': {'id': 'nm123', + 'nameText': {'text': 'Source Person'}}}]}}} + html = '' + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + archive = root / 'source.webarchive' + archive.write_bytes(plistlib.dumps({'WebMainResource': { + 'WebResourceURL': 'https://www.imdb.com/chart/starmeter/', + 'WebResourceData': html.encode()}})) + database = root / 'imdb.db' + with sqlite3.connect(database) as con: + con.execute('CREATE TABLE titles (id INTEGER PRIMARY KEY, name TEXT)') + con.execute("INSERT INTO titles VALUES (1, 'Preserved')") + result = seed.import_archive(archive, database, root / 'static') + self.assertEqual(result['features'], 1) + with sqlite3.connect(database) as con: + self.assertEqual(con.execute('SELECT kind,position FROM home_features').fetchall(), [('starmeter', 3)]) + con.execute("INSERT INTO home_features VALUES ('topic-1','topic',1,'Preserved','','','','https://www.imdb.com/','2026-09-10','{}')") + before = database.read_bytes() + seed.import_archive(archive, database, root / 'static') + self.assertEqual(database.read_bytes(), before) + + def test_import_preserves_catalog_and_second_import_preserves_bytes(self): + props = { + 'requestContext': {'timestamp': '2026-09-10T13:37:06Z'}, + 'cmsContext': {'transformedPlacements': { + 'pill-1': {'transformedArguments': { + 'displayTitle': 'Source topic', 'linkTargetUrl': '/source-topic/'}}}}, + 'pageQueryData': {'data': {'news': {'edges': []}}}, + } + html = '' + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + archive = root / 'source.webarchive' + archive.write_bytes(plistlib.dumps({'WebMainResource': { + 'WebResourceURL': 'https://www.imdb.com/', + 'WebResourceData': html.encode()}})) + database = root / 'imdb.db' + with sqlite3.connect(database) as con: + con.execute('CREATE TABLE titles (id INTEGER PRIMARY KEY, name TEXT)') + con.execute('INSERT INTO titles VALUES (1, ?)', ('Original title',)) + result = seed.import_archive(archive, database, root / 'static') + self.assertEqual(result['features'], 1) + before = database.read_bytes() + seed.import_archive(archive, database, root / 'static') + self.assertEqual(database.read_bytes(), before) + with sqlite3.connect(database) as con: + self.assertEqual(con.execute('SELECT * FROM titles').fetchall(), [(1, 'Original title')]) + self.assertEqual(con.execute('SELECT heading FROM home_features').fetchall(), [('Source topic',)]) + con.execute("INSERT INTO home_features VALUES ('birthday-nm1', 'birthday', 1, 'Person', '', '', '', 'https://www.imdb.com/name/nm1/', '2026-09-10', '{}')") + before = database.read_bytes() + seed.import_archive(archive, database, root / 'static') + self.assertEqual(database.read_bytes(), before, 'An unloaded archive must preserve prior loaded collections') + + def test_episode_labels_are_taken_from_the_source(self): + page = seed.SourcePage('') + self.assertEqual(page.episodes, [{'number': 3, 'rating': '7.9', 'title': 'Do You Reject Satan?'}]) + + def test_episode_details_keep_their_own_media_plot_and_identity(self): + page = seed.SourcePage('''
    + +
    • S2.E3
    • Thu, Sep 3, 2026
    +

    Third & Final

    +

    Actual source plot.

    ''') + self.assertEqual(page.episode_details, [{'image': 'https://m.media-amazon.com/still.jpg', + 'info': 'S2.E3 Thu, Sep 3, 2026 ', 'title': 'Third & Final ', + 'plot': 'Actual source plot. ', 'source_path': '/title/tt123/'}]) + + def test_loaded_cards_keep_optional_source_fields_optional(self): + page = seed.SourcePage(''' +
    +
    + Example & Co + 7.4
    +
    + Future show + New: Season 1 +
    Wed, Sep 16
    + ''') + self.assertEqual(page.service, 'PRIME VIDEO') + self.assertEqual([c['kind'] for c in page.cards], ['streaming', 'tv_schedule', 'birthday']) + self.assertEqual(page.cards[0]['heading'], 'Example & Co') + self.assertEqual(page.cards[0]['source_path'], '/title/tt123/') + self.assertEqual(page.cards[1]['episode_path'], '/title/tt789/') + self.assertEqual(page.cards[1]['rating'], '') + self.assertEqual(page.cards[2]['age'], '') + + +if __name__ == '__main__': + unittest.main() diff --git a/sites/imdb/tests/test_read_tasks.py b/sites/imdb/tests/test_read_tasks.py new file mode 100644 index 00000000..acb565a9 --- /dev/null +++ b/sites/imdb/tests/test_read_tasks.py @@ -0,0 +1,590 @@ +"""Synthetic read-task checks; these fixtures are not real browser-run evidence. + +The small catalog intentionally contains invented titles, reviews and ties. +It uses the app's table/column names without importing Flask or copying seed DBs. +""" + +import importlib +import json +from pathlib import Path +import shutil +import sqlite3 +import sys +import tempfile +import unittest + + +SITE = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SITE / "verify")) +verify_lib = importlib.import_module("verify_lib") +read_tasks = importlib.import_module("read_tasks") +RunEvidence = verify_lib.RunEvidence +VerificationError = verify_lib.VerificationError +TASKS = {row["id"]: row for row in ( + json.loads(line) for line in (SITE / "tasks.jsonl").read_text().splitlines() + if line.strip() +)} + +SHAW = "tt0111161" +GOD = "tt0068646" +DARK = "tt0468569" +INCEPTION = "tt1375666" +PULP = "tt0110912" +ECHO = "tt9000000" +SERIES = "tt9000001" +NOLAN = "nm0634240" + +DRAMA_SEARCH = ( + "/search/title?title_type=movie&genre=drama&rating_min=8.5&sort=rating" +) +CRIME_SEARCH = ( + "/search/title?title_type=movie&genre=crime&year_from=1990&year_to=1999" + "&rating_min=8.5&sort=rating" +) + +SCHEMA = """ +CREATE TABLE titles ( + id INTEGER PRIMARY KEY, tt_id TEXT UNIQUE, title_type TEXT, + primary_title TEXT, original_title TEXT DEFAULT '', year INTEGER, + end_year INTEGER, runtime_min INTEGER, mpaa_rating TEXT, + plot_short TEXT DEFAULT '', plot TEXT DEFAULT '', rating_avg REAL, + num_votes INTEGER DEFAULT 100, metascore INTEGER, popularity_rank INTEGER, + top_rank INTEGER, box_office_us INTEGER, box_office_world INTEGER, + box_office_opening INTEGER, budget INTEGER, release_date TEXT DEFAULT '', + country TEXT DEFAULT '', language TEXT DEFAULT '', poster_path TEXT DEFAULT '', + taglines_json TEXT DEFAULT '[]' +); +CREATE TABLE persons ( + id INTEGER PRIMARY KEY, nm_id TEXT UNIQUE, name TEXT, birth_year INTEGER, + death_year INTEGER, birth_place TEXT DEFAULT '', bio TEXT DEFAULT '', + primary_profession TEXT DEFAULT '', photo_path TEXT DEFAULT '', + known_for_json TEXT DEFAULT '[]' +); +CREATE TABLE genres (id INTEGER PRIMARY KEY, name TEXT, slug TEXT UNIQUE); +CREATE TABLE title_genre (title_id INTEGER, genre_id INTEGER, + PRIMARY KEY (title_id, genre_id)); +CREATE TABLE credits ( + id INTEGER PRIMARY KEY, title_id INTEGER, person_id INTEGER, + role TEXT, character TEXT DEFAULT '', billing_order INTEGER +); +CREATE TABLE users ( + id INTEGER PRIMARY KEY, email TEXT, name TEXT, password_hash TEXT, + created_at TEXT +); +CREATE TABLE reviews ( + id INTEGER PRIMARY KEY, title_id INTEGER, user_id INTEGER, rating INTEGER, + headline TEXT, body TEXT, helpful_count INTEGER, created_at TEXT, is_seed INTEGER +); +CREATE TABLE user_ratings ( + id INTEGER PRIMARY KEY, user_id INTEGER, title_id INTEGER, + rating INTEGER, created_at TEXT +); +CREATE TABLE watchlist_items ( + id INTEGER PRIMARY KEY, user_id INTEGER, title_id INTEGER, added_at TEXT +); +""" + +# The numeric facts are fixture inputs, not claims about the live IMDb website. +TITLES = [ + (1, SHAW, "movie", "The Shawshank Redemption", 1994, 142, "R", 9.3, 1, + 28767189, 28884504, 727327, 25000000), + (2, GOD, "movie", "The Godfather", 1972, 175, "R", 9.2, 2, + 136381073, 246120974, 302393, 6000000), + (3, DARK, "movie", "The Dark Knight", 2008, 152, "PG-13", 9.0, 3, + 534858444, 1005973645, 158411483, 185000000), + (4, INCEPTION, "movie", "Inception", 2010, 148, "PG-13", 8.8, 6, + 292576195, 836848102, 62785337, 160000000), + (5, PULP, "movie", "Pulp Fiction", 1994, 154, "R", 8.9, 4, + 107928762, 213928762, 93117882, 8000000), + (6, ECHO, "movie", "Synthetic Cutoff Echo", 1999, 100, "R", 8.9, 5, + 15000000, 32500000, 2000000, 4500000), + (7, SERIES, "tvSeries", "Synthetic Crime Series", 2008, 45, "TV-MA", 9.5, 1, + None, None, None, None), + (8, "tt9000002", "tvSeries", "Synthetic Nolan Television", 2020, 50, + "TV-MA", 9.9, 2, None, None, None, None), + (9, "tt9000003", "movie", "Synthetic Writer Only Feature", 2021, 90, + "PG", 9.7, None, 1000000, 3000000, 100000, 500000), +] + +ANSWERS = { + 0: ("#1 The Shawshank Redemption: 142 minutes, R.\n" + "#3 The Dark Knight: 152 minutes, PG-13.\n" + "The Dark Knight has the longer runtime."), + 2: "The Dark Knight is first in cumulative domestic gross; production budget $185 million.", + 7: "The Dark Knight (2008), IMDb rating 9.0/10.", + 9: "1. The Shawshank Redemption\n2. The Godfather\n3. The Dark Knight", + 10: "The Shawshank Redemption: directed by Frank Darabont; worldwide gross $28.9M.", + 12: ("Movie: The Shawshank Redemption (1994), IMDb rating 9.3.\n" + "TV series: Synthetic Crime Series (2008), IMDb rating 9.5.\n" + "The TV series group has the higher top rating."), + 14: ("The Dark Knight: IMDb rating 9.0; worldwide gross $1.0B.\n" + "Inception: IMDb rating 8.8; worldwide gross $836.8M.\n" + "The Dark Knight has the higher rating and the larger gross."), +} + +PATHS = { + 0: ["/chart/top", f"/title/{DARK}", f"/title/{SHAW}"], + 2: ["/chart/boxoffice", f"/title/{DARK}"], + 7: [f"/name/{NOLAN}", f"/title/{DARK}", f"/title/{INCEPTION}"], + 9: [DRAMA_SEARCH], + 10: [CRIME_SEARCH, f"/title/{SHAW}"], + 12: ["/genre/crime", f"/title/{SHAW}", f"/title/{SERIES}"], + 14: [f"/title/{INCEPTION}", f"/title/{DARK}"], +} + + +class SyntheticReadTaskTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.run_dir = Path(self.temp.name) + self.origin = "http://synthetic-read.localhost:48015" + before = self.run_dir / "before.db" + with sqlite3.connect(before) as db: + db.executescript(SCHEMA) + db.executemany( + "INSERT INTO titles (id,tt_id,title_type,primary_title,year," + "runtime_min,mpaa_rating,rating_avg,top_rank,box_office_us," + "box_office_world,box_office_opening,budget) " + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", TITLES) + db.executemany("INSERT INTO persons (id,nm_id,name,birth_year) VALUES (?,?,?,?)", [ + (1, "nm0001104", "Frank Darabont", 1959), + (2, "nm0000338", "Francis Ford Coppola", 1939), + (3, NOLAN, "Christopher Nolan", 1970), + (6, "nm0000233", "Quentin Tarantino", 1963), + (7, "nm9000001", "Synthetic Director", 1960), + ]) + db.executemany("INSERT INTO genres VALUES (?,?,?)", [ + (1, "Drama", "drama"), (2, "Crime", "crime"), (3, "Action", "action")]) + db.executemany("INSERT INTO title_genre VALUES (?,?)", [ + (1, 1), (1, 2), (2, 1), (2, 2), (3, 1), (3, 2), + (4, 1), (4, 3), (5, 1), (5, 2), (6, 1), (6, 2), + (7, 1), (7, 2), (8, 3), (9, 3), + ]) + db.executemany( + "INSERT INTO credits (title_id,person_id,role,character,billing_order) " + "VALUES (?,?,?,?,?)", [ + (1, 1, "director", "", None), + (2, 2, "director", "", None), + (3, 3, "director", "", None), + (4, 3, "director", "", None), + (5, 6, "director", "", None), + (6, 7, "director", "", None), + (8, 3, "director", "", None), + (9, 3, "writer", "", None), + (9, 7, "director", "", None), + ]) + db.executemany("INSERT INTO users VALUES (?,?,?,?,?)", [ + (1, "synthetic-alice@example.test", "Synthetic Alice", "fixture-hash", "2020-01-01"), + (2, "synthetic-bob@example.test", "Synthetic Bob", "fixture-hash", "2020-01-01"), + ]) + shutil.copyfile(before, self.run_dir / "after.db") + + def edit_catalog(self, sql, parameters=()): + """Change both snapshots: task truth changes, but business state does not.""" + for filename in ("before.db", "after.db"): + with sqlite3.connect(self.run_dir / filename) as db: + db.execute(sql, parameters) + + def check(self, number, answer=None, paths=None): + task_id = f"IMDb--{number}" + selected_paths = PATHS[number] if paths is None else paths + trajectory = { + "task_id": task_id, + "task": TASKS[task_id]["ques"], + "start_url": self.origin + "/", + "final_answer": ANSWERS[number] if answer is None else answer, + "fixture_kind": "synthetic", + "steps": [{"url": path if "://" in path else self.origin + path, + "status": "completed", "action": "observe"} + for path in selected_paths], + } + (self.run_dir / "trajectory.json").write_text(json.dumps(trajectory)) + with RunEvidence(self.run_dir, task_id, expected_ques=TASKS[task_id]["ques"]) as run: + return read_tasks.check_read_task(number, run) + + def assert_accepts(self, number, answer=None, paths=None): + self.assertIsInstance(self.check(number, answer, paths), list) + + def assert_rejects(self, number, answer=None, paths=None): + with self.assertRaises(VerificationError): + self.check(number, answer, paths) + + def test_task0_ranked_films_can_be_visited_in_either_order(self): + self.assert_accepts(0) + self.assert_accepts(0, paths=["/chart/top", f"/title/{SHAW}", f"/title/{DARK}"]) + + def test_task2_uses_catalog_cumulative_domestic_gross(self): + self.assert_accepts(2) + # A larger opening weekend does not change the cumulative chart winner. + self.edit_catalog("UPDATE titles SET box_office_opening=999000000 WHERE tt_id=?", (PULP,)) + self.assert_accepts(2) + + def test_task7_director_movies_exclude_writer_credit_and_tv(self): + self.assert_accepts(7) + + def test_task9_top_three_from_results_page(self): + self.assert_accepts(9) + + def test_task10_crime_nineties_director_and_worldwide(self): + self.assert_accepts(10) + + def test_task12_compares_movie_and_tv_top_ratings(self): + self.assert_accepts(12) + + def test_task14_values_and_both_comparisons_in_either_visit_order(self): + self.assert_accepts(14) + self.assert_accepts(14, paths=list(reversed(PATHS[14]))) + + def test_every_read_task_requires_relevant_navigation(self): + for number in ANSWERS: + with self.subTest(number=number): + self.assert_rejects(number, paths=[]) + + def test_foreign_urls_cannot_supply_the_required_navigation(self): + for number, paths in PATHS.items(): + with self.subTest(number=number): + self.assert_rejects(number, paths=["http://foreign.example" + path for path in paths]) + + def test_every_read_task_rejects_business_state_changes(self): + with sqlite3.connect(self.run_dir / "after.db") as db: + db.execute("UPDATE users SET name='Unexpected edit' WHERE id=1") + for number in ANSWERS: + with self.subTest(number=number): + self.assert_rejects(number) + + def test_wrong_or_partial_answers_are_rejected(self): + examples = { + 0: "The Shawshank Redemption: 152 minutes, PG-13.\nThe Dark Knight: 142 minutes, R.\nThe Shawshank Redemption is longer.", + 2: "The Godfather ranks first; budget $6 million.", + 7: "Synthetic Writer Only Feature (2021), rating 9.7.", + 9: "The Godfather\nThe Dark Knight\nPulp Fiction", + 10: "The Shawshank Redemption: worldwide gross $28.9M.", + 12: "Movie: The Shawshank Redemption (1994), rating 9.5.\nTV: Synthetic Crime Series (2008), rating 9.3.\nMovies have the higher rating.", + 14: "The Dark Knight: rating 9.0, worldwide gross $836.8M.\nInception: rating 8.8, worldwide gross $1.0B.\nThe Dark Knight has the higher rating; Inception has the larger gross.", + } + for number, answer in examples.items(): + with self.subTest(number=number): + self.assert_rejects(number, answer) + + def test_money_accepts_exact_amounts_and_equivalent_units(self): + answers = [ + "The Dark Knight: production budget USD 185,000,000.", + "The Dark Knight: production budget 185 million dollars.", + "The Dark Knight: production budget $185000K.", + ] + for answer in answers: + with self.subTest(answer=answer): + self.assert_accepts(2, answer) + + def test_another_money_field_cannot_supply_the_budget(self): + for answer in ( + "The Dark Knight: worldwide gross $185M.", + "The Dark Knight: opening weekend US & Canada $185M.", + ): + with self.subTest(answer=answer): + self.assert_rejects(2, answer) + self.assert_accepts(2, "The Dark Knight: $185M.") + + def test_markdown_rows_preserve_runtime_and_classification_binding(self): + self.assert_accepts(0, """| Rank | Title | Runtime | MPAA | +| --- | --- | --- | --- | +| 1 | The Shawshank Redemption | 142 minutes | R | +| 3 | The Dark Knight | 152 minutes | PG-13 | + +The Dark Knight has the longer runtime. +""") + + def test_markdown_money_columns_and_explicit_comparisons(self): + self.assert_accepts(14, """| Field | The Dark Knight | Inception | +| --- | --- | --- | +| IMDb rating | 9.0 | 8.8 | +| Worldwide gross | $1.0 billion | $836.8 million | + +The Dark Knight has the higher IMDb rating and the larger worldwide gross. +""") + + def test_catalog_changes_drive_truth_instead_of_hardcoded_answers(self): + self.edit_catalog("UPDATE titles SET budget=187000000 WHERE tt_id=?", (DARK,)) + self.assert_rejects(2) + self.assert_accepts(2, "The Dark Knight is first; production budget $187M.") + + def test_task0_requires_both_title_details_and_chart(self): + for paths in (["/chart/top", f"/title/{SHAW}"], [f"/title/{SHAW}", f"/title/{DARK}"]): + with self.subTest(paths=paths): + self.assert_rejects(0, paths=paths) + + def test_task0_explicit_word_ranks_keep_their_title_binding(self): + answer = ("First-ranked movie: The Shawshank Redemption, 142 minutes, R.\n" + "Third-ranked movie: The Dark Knight, 152 minutes, PG-13.\n" + "The Dark Knight has the longer runtime.") + self.assert_accepts(0, answer) + self.assert_rejects(0, answer.replace("First-ranked", "Third-ranked", 1) + .replace("Third-ranked movie: The Dark Knight", "First-ranked movie: The Dark Knight")) + table = ("| Rank | Title | Runtime | MPAA |\n| --- | --- | --- | --- |\n" + "| 3 | The Shawshank Redemption | 142 minutes | R |\n" + "| 1 | The Dark Knight | 152 minutes | PG-13 |\n" + "The Dark Knight has the longer runtime.") + self.assert_rejects(0, table) + + def test_task0_genuine_single_line_answer_with_runtime_difference(self): + # Frozen candidate task0/run-01 wording; these DB/navigation fixtures are synthetic. + self.assert_accepts(0, "#1: The Shawshank Redemption — 142 minutes, MPAA R. " + "#3: The Dark Knight — 152 minutes, MPAA PG-13. " + "The Dark Knight has the longer runtime, by 10 minutes.") + + def test_task0_prefix_ranks_bind_forward_in_single_and_multiple_lines(self): + for first, third in (("#1:", "#3:"), ("rank 1:", "rank 3:"), + ("First-ranked movie:", "Third-ranked movie:")): + for separator in (" ", "\n"): + with self.subTest(first=first, separator=separator): + answer = (f"{first} The Shawshank Redemption: 142 minutes, R." + separator + + f"{third} The Dark Knight: 152 minutes, PG-13." + separator + + "The Dark Knight has the longer runtime.") + self.assert_accepts(0, answer) + wrong = (f"{third} The Shawshank Redemption: 142 minutes, R." + separator + + f"{first} The Dark Knight: 152 minutes, PG-13." + separator + + "The Dark Knight has the longer runtime.") + self.assert_rejects(0, wrong) + + def test_task0_suffix_and_mixed_rank_positions_still_bind_correctly(self): + for first, third in (("The Shawshank Redemption, rank 1", "The Dark Knight, rank 3"), + ("The Shawshank Redemption, rank 1", "#3: The Dark Knight"), + ("#1: The Shawshank Redemption", "The Dark Knight, rank 3")): + for separator in (" ", "\n"): + with self.subTest(first=first, third=third, separator=separator): + answer = (first + ": 142 minutes, R." + separator + third + + ": 152 minutes, PG-13. The Dark Knight has the longer runtime.") + self.assert_accepts(0, answer) + self.assert_rejects(0, answer.replace("rank 1", "rank 3").replace("#1:", "#3:")) + + def test_task0_difference_is_distinct_from_runtime_and_must_be_correct(self): + facts = ("#1: The Shawshank Redemption: 142 minutes, R.\n" + "#3: The Dark Knight: 152 minutes, PG-13.\n") + for comparison in ("The Dark Knight has the longer runtime, by {delta} minutes.", + "The Dark Knight is longer by {delta} minutes.", + "The Shawshank Redemption is shorter by {delta} minutes."): + with self.subTest(comparison=comparison): + self.assert_accepts(0, facts + comparison.format(delta=10)) + for wrong in (9, 11, 142, 152): + self.assert_rejects(0, facts + comparison.format(delta=wrong)) + + def test_task0_reversed_runtimes_are_not_rescued_by_correct_difference(self): + self.assert_rejects(0, "#1: The Shawshank Redemption: 152 minutes, R. " + "#3: The Dark Knight: 142 minutes, PG-13. " + "The Dark Knight has the longer runtime, by 10 minutes.") + + def test_task12_single_line_type_prefixes_stay_with_their_movies(self): + movie = "Highest-rated Crime movie: The Shawshank Redemption (1994), 9.3/10." + series = "Highest-rated Crime TV series: Synthetic Crime Series (2008), 9.5/10." + comparison = "Neither group has a tie. The TV-series group has the higher top rating, by 0.2 points." + for first, second in ((movie, series), (series, movie)): + for separator in (" ", "\n"): + with self.subTest(first=first, separator=separator): + self.assert_accepts(12, separator.join((first, second, comparison))) + + def test_task12_reversed_type_prefixes_are_not_rescued_by_correct_values(self): + movie = "Highest-rated Crime TV series: The Shawshank Redemption (1994), 9.3/10." + series = "Highest-rated Crime movie: Synthetic Crime Series (2008), 9.5/10." + for separator in (" ", "\n"): + self.assert_rejects(12, separator.join((movie, series, "The TV-series group has the higher top rating."))) + + def test_task12_inline_extra_winner_and_wrong_comparison_are_rejected(self): + facts = ("Highest-rated Crime movie: The Shawshank Redemption (1994), 9.3/10. " + "Highest-rated Crime TV series: Synthetic Crime Series (2008), 9.5/10. ") + self.assert_rejects(12, facts + "Highest-rated Crime movie: The Godfather (1972), 9.2/10. " + "The TV-series group has the higher top rating.") + self.assert_rejects(12, facts + "The movie group has the higher top rating.") + + def test_explicit_director_assertions_reject_extra_people(self): + self.assert_rejects(10, "The Shawshank Redemption: directed by Frank Darabont and Christopher Nolan; worldwide gross $28.9M.") + self.assert_rejects(10, "The Shawshank Redemption: directed by Frank Darabont and John Example; worldwide gross $28.9M.") + self.assert_rejects(10, "The Shawshank Redemption: directors: Frank Darabont, John Example; worldwide gross $28.9M.") + self.assert_rejects(10, "The Shawshank Redemption: directors are Frank Darabont and Christopher Nolan; worldwide gross $28.9M.") + self.assert_accepts(10, "The Shawshank Redemption: directed by Darabont, not Christopher Nolan; worldwide gross $28.9M.") + self.assert_accepts(10, "The Shawshank Redemption: directed by Frank Darabont, not John Example; worldwide gross $28.9M.") + self.assert_accepts(10, "The Shawshank Redemption: directed by Frank Darabont rather than Christopher Nolan; worldwide gross $28.9M.") + self.assert_accepts(10, ANSWERS[10] + "\nChristopher Nolan directed Inception.") + self.assert_accepts(10, ANSWERS[10] + "\nPulp Fiction was directed by Quentin Tarantino, but is a lower-rated result.") + + def test_director_full_names_do_not_match_unrelated_surnames(self): + self.edit_catalog("INSERT INTO persons (id,nm_id,name) VALUES (8,'nm9000008','Clive Frank')") + self.edit_catalog("INSERT INTO persons (id,nm_id,name) VALUES (9,'nm9000009','Caroline Quentin')") + self.assert_accepts(10) + self.edit_catalog("UPDATE titles SET rating_avg=9.4 WHERE tt_id=?", (PULP,)) + self.assert_accepts(10, "Pulp Fiction: directed by Quentin Tarantino; worldwide gross $213.9M.", + [CRIME_SEARCH, f"/title/{PULP}"]) + + def test_highest_result_sets_reject_extra_winners_not_loser_explanations(self): + extra = "Pulp Fiction: directed by Quentin Tarantino; worldwide gross $213.9M." + self.assert_rejects(10, "Highest-rated matches:\n" + ANSWERS[10] + "\n" + extra) + self.assert_rejects(10, ANSWERS[10] + "\nPulp Fiction is also tied for the highest rating.") + self.assert_accepts(10, ANSWERS[10] + "\nPulp Fiction is not tied for the highest rating.") + self.assert_accepts(10, "Highest-rated matches:\n" + ANSWERS[10] + + "\nPulp Fiction is a lower-rated candidate, not a highest-rated match.") + self.assert_accepts(10, "Highest-rated matches:\n" + ANSWERS[10] + + "\n\nOther results:\n" + extra) + false_movie = ("Top movies: The Shawshank Redemption (1994), IMDb rating 9.3.\n" + "Top movies: The Godfather (1972), IMDb rating 9.2.\n" + "TV series: Synthetic Crime Series (2008), IMDb rating 9.5.\n" + "The TV series group has the higher top rating.") + self.assert_rejects(12, false_movie) + self.assert_accepts(12, ANSWERS[12] + "\nThe Godfather (1972), rating 9.2, is below the highest-rated movie.") + self.assert_accepts(12, ANSWERS[12] + "\nThe Godfather is not tied for highest.") + + def test_task7_all_director_movie_rating_ties(self): + self.edit_catalog("UPDATE titles SET rating_avg=9.0 WHERE tt_id=?", (INCEPTION,)) + self.assert_rejects(7) + self.assert_accepts(7, "The Dark Knight (2008): 9.0.\nInception (2010): 9.0.") + self.assert_rejects(7, "The Dark Knight (2010): 9.0.\nInception (2008): 9.0.") + + def test_task9_filters_must_all_be_present_and_correct(self): + for query in ( + DRAMA_SEARCH.replace("title_type=movie&", ""), + DRAMA_SEARCH.replace("genre=drama&", ""), + DRAMA_SEARCH.replace("rating_min=8.5&", ""), + DRAMA_SEARCH.replace("sort=rating", "sort=popularity"), + DRAMA_SEARCH.replace("title_type=movie", "title_type=tvSeries"), + DRAMA_SEARCH.replace("genre=drama", "genre=crime"), + DRAMA_SEARCH.replace("rating_min=8.5", "rating_min=8.0"), + DRAMA_SEARCH + "&genre=crime", + ): + with self.subTest(query=query): + self.assert_rejects(9, paths=[query]) + self.assert_accepts(9, paths=[DRAMA_SEARCH.replace("8.5", "8.50")]) + + def test_task9_requires_exactly_three_in_descending_order(self): + for answer in ( + "The Shawshank Redemption\nThe Godfather", + "The Dark Knight\nThe Godfather\nThe Shawshank Redemption", + "The Shawshank Redemption\nThe Godfather\nThe Dark Knight\nPulp Fiction", + ): + with self.subTest(answer=answer): + self.assert_rejects(9, answer) + + def test_task9_any_third_place_tie_selection_keeps_all_higher_titles(self): + # Bring the third movie into the existing 8.9 cutoff tie. + self.edit_catalog("UPDATE titles SET rating_avg=8.9 WHERE tt_id=?", (DARK,)) + for third in ("The Dark Knight", "Pulp Fiction", "Synthetic Cutoff Echo"): + with self.subTest(third=third): + self.assert_accepts(9, "The Shawshank Redemption\nThe Godfather\n" + third) + self.assert_rejects(9, "The Shawshank Redemption\nPulp Fiction\nSynthetic Cutoff Echo") + + def test_task9_order_within_an_included_tie_is_irrelevant(self): + self.edit_catalog("UPDATE titles SET rating_avg=9.2 WHERE tt_id=?", (DARK,)) + self.assert_accepts(9, "The Shawshank Redemption\nThe Dark Knight\nThe Godfather") + + def test_task10_all_search_constraints_are_required(self): + for query in ( + CRIME_SEARCH.replace("year_from=1990&", ""), + CRIME_SEARCH.replace("year_to=1999&", ""), + CRIME_SEARCH.replace("year_from=1990", "year_from=1991"), + CRIME_SEARCH.replace("year_to=1999", "year_to=1998"), + CRIME_SEARCH.replace("genre=crime", "genre=drama"), + CRIME_SEARCH.replace("rating_min=8.5", "rating_min=9.0"), + CRIME_SEARCH.replace("sort=rating", "sort=year"), + ): + with self.subTest(query=query): + self.assert_rejects(10, paths=[query, f"/title/{SHAW}"]) + + def test_task10_inclusive_year_endpoints_and_all_top_ties(self): + self.edit_catalog("UPDATE titles SET year=1990 WHERE tt_id=?", (SHAW,)) + self.edit_catalog("UPDATE titles SET rating_avg=9.3 WHERE tt_id=?", (ECHO,)) + paths = [CRIME_SEARCH, f"/title/{SHAW}", f"/title/{ECHO}"] + self.assert_rejects(10, paths=paths) + self.assert_accepts(10, ANSWERS[10] + "\nSynthetic Cutoff Echo: directed by Synthetic Director; worldwide gross $32.5M.", paths) + + def test_task10_all_directors_must_be_reported(self): + self.edit_catalog("INSERT INTO credits (title_id,person_id,role) VALUES (1,7,'director')") + self.assert_rejects(10) + self.assert_accepts(10, "The Shawshank Redemption: directed by Frank Darabont and Synthetic Director; worldwide gross $28.9M.") + + def test_task12_all_within_group_ties_and_tied_comparison(self): + self.edit_catalog("UPDATE titles SET rating_avg=9.3 WHERE tt_id IN (?,?)", (GOD, SERIES)) + answer = ("Movies: The Shawshank Redemption (1994), rating 9.3.\n" + "Movie: The Godfather (1972), rating 9.3.\n" + "TV series: Synthetic Crime Series (2008), rating 9.3.\n" + "The movie and TV series groups are tied at 9.3.") + self.assert_accepts(12, answer) + self.assert_rejects(12, answer.replace("Movie: The Godfather (1972), rating 9.3.\n", "")) + + def test_task12_wrong_comparison_cannot_hide_behind_correct_values(self): + answer = ANSWERS[12].replace("TV series group has the higher", "movie group has the higher") + self.assert_rejects(12, answer) + + def test_task14_ties_are_reported_explicitly(self): + self.edit_catalog("UPDATE titles SET rating_avg=9.0, box_office_world=1005973645 WHERE tt_id=?", (INCEPTION,)) + answer = ("The Dark Knight: IMDb rating 9.0; worldwide gross $1.0B.\n" + "Inception: IMDb rating 9.0; worldwide gross $1.0B.\n" + "Both movies tie in rating and worldwide gross.") + self.assert_accepts(14, answer) + self.assert_rejects(14, answer.replace("Both movies tie in rating and worldwide gross.", + "The Dark Knight has the higher rating and larger gross.")) + + def test_fields_on_another_title_cannot_complete_the_winner(self): + self.assert_rejects(10, "The Shawshank Redemption is the highest-rated result.\n" + "Pulp Fiction: directed by Frank Darabont; worldwide gross $28.9M.") + self.assert_rejects(2, "The Dark Knight is first. Inception: production budget $185M.") + + def test_comparison_in_second_clause_keeps_its_own_subject(self): + facts = ANSWERS[14].rsplit("\n", 1)[0] + for comparison in ( + "The Dark Knight has the higher rating, while Inception has the larger worldwide gross.", + "The Dark Knight has the higher rating and Inception has the larger worldwide gross.", + "The Dark Knight has the higher rating; Inception has the larger worldwide gross.", + ): + with self.subTest(comparison=comparison): + self.assert_rejects(14, facts + "\n" + comparison) + self.assert_accepts(14, facts + "\nThe Dark Knight has the higher rating than Inception and the larger worldwide gross.") + self.assert_accepts(14, facts + "\nInception has a lower rating and a smaller worldwide gross than The Dark Knight.") + + def test_comparison_winner_table_and_missing_conclusion(self): + facts = ANSWERS[14].rsplit("\n", 1)[0] + table = "\n| Metric | Winner |\n| --- | --- |\n| Rating | The Dark Knight |\n| Worldwide gross | The Dark Knight |" + self.assert_accepts(14, facts + table) + self.assert_rejects(14, facts + table.replace("| Worldwide gross | The Dark Knight |", "| Worldwide gross | Inception |")) + self.assert_rejects(14, facts) + self.assert_rejects(0, ANSWERS[0].rsplit("\n", 1)[0]) + self.assert_rejects(12, ANSWERS[12].rsplit("\n", 1)[0]) + + def test_extra_false_rating_tie_is_not_accepted(self): + self.assert_rejects(7, "The Dark Knight (2008), IMDb rating 9.0/10.\n" + "Inception (2010), IMDb rating 9.0/10.\nThese films tie for the highest rating.") + self.assert_accepts(7, "The Dark Knight (2008), IMDb rating 9.0/10.\n" + "Inception (2010) has a lower rating of 8.8/10.") + + def test_correct_negative_tie_explanation_is_allowed(self): + self.assert_accepts(7, "The Dark Knight (2008), IMDb rating 9.0/10.\n" + "Inception (2010), IMDb rating 8.8/10; it is not tied for the highest rating.") + self.assert_rejects(7, "The Dark Knight (2008), IMDb rating 9.0/10.\n" + "Inception (2010), IMDb rating 8.8/10; it is not higher, but is tied for highest.") + + def test_director_rating_can_come_from_other_public_listings(self): + for page in ("/chart/boxoffice", "/chart/top", "/", "/find?q=Nolan&s=tt", DRAMA_SEARCH): + with self.subTest(page=page): + self.assert_accepts(7, paths=[f"/name/{NOLAN}", page]) + self.edit_catalog("UPDATE persons SET known_for_json=? WHERE nm_id=?", (json.dumps([DARK]), NOLAN)) + self.assert_accepts(7, paths=[f"/name/{NOLAN}"]) + + def test_empty_advanced_results_do_not_prove_a_director_movie_rating(self): + self.assert_rejects(7, paths=[f"/name/{NOLAN}", DRAMA_SEARCH + "&genre=drama"]) + self.assert_rejects(7, paths=[f"/name/{NOLAN}", DRAMA_SEARCH.replace("8.5", "9.5")]) + + def test_multi_line_fact_blocks_and_group_headings(self): + self.assert_accepts(14, "The Dark Knight\nIMDb rating: 9.0\nWorldwide gross: $1 billion\n\n" + "Inception\nIMDb rating: 8.8\nWorldwide gross: $836.8 million\n\n" + "The Dark Knight wins both comparisons.") + self.assert_accepts(12, "Movies:\nThe Shawshank Redemption (1994): rating 9.3.\n\n" + "TV series:\nSynthetic Crime Series (2008): rating 9.5.\n\n" + "The TV group has the higher top rating.") + + def test_filter_duplicate_and_equivalent_query_values(self): + for query in (DRAMA_SEARCH + "&genre=drama", DRAMA_SEARCH + "&rating_min=9.0", + DRAMA_SEARCH + "&title_type=tvSeries", DRAMA_SEARCH + "&year_to=2000"): + with self.subTest(query=query): + self.assert_rejects(9, paths=[query]) + self.assert_accepts(9, paths=[DRAMA_SEARCH + "&rating_min=8.50&year_from=&year_to="]) + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/imdb/tests/test_seed_data.py b/sites/imdb/tests/test_seed_data.py new file mode 100644 index 00000000..55f162b0 --- /dev/null +++ b/sites/imdb/tests/test_seed_data.py @@ -0,0 +1,195 @@ +import hashlib +import json +from pathlib import Path +import sqlite3 +import sys +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import Mock, patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from migrate_seed import migrate_seed +from seed_data import _ld_nm_id, _parse_release_date, _release_date_from_scrape +import seed_data + + +class SourceParsingTests(unittest.TestCase): + def test_seed_rejects_wrong_missing_and_malformed_person_identity(self): + with tempfile.TemporaryDirectory() as directory: + scraped = Path(directory) + for nm_id, url in ( + ('nm1165110', 'https://www.imdb.com/name/nm1165110/'), + ('nm0000245', 'https://www.imdb.com/name/nm1165110/'), + ('nm0000001', ''), + ('nm0000002', 'https://www.imdb.com/name/nm0000002wrong/')): + (scraped / f'name_{nm_id}.json').write_text(json.dumps( + {'h1': 'Chris Hemsworth', 'ld': {'url': url}})) + database = Mock() + database.session.query.return_value.count.return_value = 0 + person = Mock(side_effect=lambda **values: SimpleNamespace(**values, credits=[])) + models = [Mock() for _ in range(8)] + with patch.object(seed_data, 'SCRAPED', scraped), patch('builtins.print'): + seed_data.seed_all(database, models[0], person, *models[1:]) + self.assertEqual(person.call_count, 1) + self.assertEqual(person.call_args.kwargs['nm_id'], 'nm1165110') + + def test_person_canonical_url_is_an_exact_identity(self): + self.assertEqual(_ld_nm_id({'url': 'https://www.imdb.com/name/nm1165110/'}), 'nm1165110') + self.assertNotEqual(_ld_nm_id({'url': 'https://www.imdb.com/name/nm0000245/'}), 'nm1165110') + for url in ('', '/title/tt1165110/', '/name/nm1165110garbage/', 42): + with self.subTest(url=url): + self.assertIsNone(_ld_nm_id({'url': url})) + self.assertIsNone(_ld_nm_id({})) + + def test_complete_dates_only_and_structured_source_wins(self): + self.assertEqual(_parse_release_date('2008-07-18'), '2008-07-18') + self.assertEqual(_parse_release_date('Release date | July 18, 2008 (United States)'), '2008-07-18') + self.assertEqual(_parse_release_date('September 1, 2000 (Italy)'), '2000-09-01') + self.assertEqual(_release_date_from_scrape( + {'datePublished': '2008-07-14'}, {'releasedate': 'July 18, 2008 (United States)'}), '2008-07-14') + self.assertEqual(_release_date_from_scrape( + {'datePublished': '2008-02-30'}, {'releaseDate': 'July 18, 2008'}), '2008-07-18') + for value in ('2008', '2008-07', 'Release date | July 18, 20', + 'Release date | February 30, 2008', '2008-07-180', 'Unknown'): + with self.subTest(value=value): + self.assertEqual(_parse_release_date(value), '') + + +class SeedMigrationTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.db_path = Path(self.temp.name) / 'imdb.db' + with sqlite3.connect(self.db_path) as connection: + connection.executescript(''' + CREATE TABLE persons (id INTEGER PRIMARY KEY, nm_id TEXT UNIQUE, + name TEXT, birth_year INTEGER, death_year INTEGER, + birth_place TEXT, bio TEXT, primary_profession TEXT, + photo_path TEXT, known_for_json TEXT); + CREATE TABLE titles (id INTEGER PRIMARY KEY, tt_id TEXT, + primary_title TEXT, release_date TEXT, rating_avg REAL, year INTEGER); + CREATE TABLE credits (id INTEGER PRIMARY KEY, title_id INTEGER, + person_id INTEGER, character TEXT); + CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT); + CREATE TABLE user_ratings (id INTEGER PRIMARY KEY, user_id INTEGER, + title_id INTEGER, rating INTEGER); + INSERT INTO persons VALUES (1, 'nm1165110', 'Robin Williams', 1951, 2014, + 'unverified place', 'unverified biography', 'Actor', 'nm1165110.jpg', '["tt0000001"]'); + INSERT INTO persons VALUES (2, 'nm0850696', 'Matt Tarses', 1966, NULL, + '', 'retain biography', 'Producer', 'retain.jpg', '[]'); + INSERT INTO persons VALUES (3, 'nm0001855', 'Tom Wilson(LXXXVI)', NULL, NULL, + '', 'retain variant', 'Actor', 'variant.jpg', '[]'); + INSERT INTO titles VALUES (1, 'tt4154796', 'Avengers: Endgame', + 'Release date | April 26, 2019 (United States)', 8.4, 2019); + INSERT INTO titles VALUES (2, 'tt0000002', 'Partial source', 'Release date | May 2, 20', 5.0, 2020); + INSERT INTO titles VALUES (3, 'tt0209144', 'Memento', 'May 25, 2001 (United States)', 8.4, 2001); + INSERT INTO titles VALUES (4, 'tt0108052', 'Schindler''s List', 'February 4, 1994 (United States)', 9.0, 1994); + INSERT INTO credits VALUES (1, 1, 1, 'Thor'); + INSERT INTO users VALUES (1, 'seed user'); + INSERT INTO user_ratings VALUES (1, 1, 1, 8); + ''') + self.manifest = Path(self.temp.name) / 'corrections.json' + self.manifest.write_text(json.dumps({ + 'schema_version': 1, + 'source_seed': {'sha256': hashlib.sha256(self.db_path.read_bytes()).hexdigest()}, + 'people': [{'nconst': 'nm1165110', 'primaryName': 'Chris Hemsworth', + 'birthYear': '1983', 'deathYear': '\\N', + 'primaryProfession': 'actor,producer,soundtrack', + 'knownForTitles': 'tt4154796,tt0800369', + 'kind': 'profile_collision'}], + 'birth_year_corrections': [{'nconst': 'nm0850696', 'birthYear': '1968'}], + })) + + def rows(self, table): + with sqlite3.connect(self.db_path) as connection: + return connection.execute(f'SELECT * FROM {table} ORDER BY id').fetchall() + + def test_migration_preserves_relationships_and_unrelated_state(self): + protected = {table: self.rows(table) for table in ('credits', 'users', 'user_ratings')} + old_people = self.rows('persons') + old_titles = self.rows('titles') + result = migrate_seed(self.db_path, self.manifest) + for table, rows in protected.items(): + self.assertEqual(self.rows(table), rows) + people = self.rows('persons') + self.assertEqual(people[0], (1, 'nm1165110', 'Chris Hemsworth', 1983, None, + '', '', 'Actor, Producer, Soundtrack', '', + '["tt4154796", "tt0800369"]')) + self.assertEqual(people[1], old_people[1][:3] + (1968,) + old_people[1][4:]) + self.assertEqual(people[2], old_people[2]) + self.assertEqual(self.rows('titles')[0], old_titles[0][:3] + ('2019-04-26',) + old_titles[0][4:]) + self.assertEqual(self.rows('titles')[1], old_titles[1]) + self.assertEqual(result['changed_rows'], {'persons': 2, 'titles': 3}) + self.assertEqual(result['protected_tables_unchanged'], ['credits', 'user_ratings', 'users']) + + def test_second_migration_is_byte_identical(self): + migrate_seed(self.db_path, self.manifest) + before = self.db_path.read_bytes() + result = migrate_seed(self.db_path, self.manifest) + self.assertEqual(result['changed_rows'], {}) + self.assertEqual(self.db_path.read_bytes(), before) + + def test_unexpected_source_is_rejected_without_writes(self): + with sqlite3.connect(self.db_path) as connection: + connection.execute("UPDATE users SET name='different state'") + before = self.db_path.read_bytes() + with self.assertRaisesRegex(ValueError, 'source seed'): + migrate_seed(self.db_path, self.manifest) + self.assertEqual(self.db_path.read_bytes(), before) + + def add_title_corrections(self, include_current_source=False): + manifest = json.loads(self.manifest.read_text()) + manifest['title_year_corrections'] = [ + {'tconst': 'tt0209144', 'before': 2001, 'after': 2000}, + {'tconst': 'tt0108052', 'before': 1994, 'after': 1993}, + ] + if include_current_source: + manifest['additional_source_seeds'] = [ + {'sha256': hashlib.sha256(self.db_path.read_bytes()).hexdigest()}] + self.manifest.write_text(json.dumps(manifest)) + + def test_original_source_combines_date_and_canonical_year_corrections(self): + self.add_title_corrections() + result = migrate_seed(self.db_path, self.manifest) + rows = self.rows('titles') + self.assertEqual(rows[2][3:], ('2001-05-25', 8.4, 2000)) + self.assertEqual(rows[3][3:], ('1994-02-04', 9.0, 1993)) + title_changes = {item['key']: item['fields'] for item in result['logical_diff'] + if item['table'] == 'titles'} + for identity in ('tt0209144', 'tt0108052'): + self.assertEqual(title_changes[identity], ['release_date', 'year']) + before = self.db_path.read_bytes() + self.assertEqual(migrate_seed(self.db_path, self.manifest)['changed_rows'], {}) + self.assertEqual(self.db_path.read_bytes(), before) + + def test_previous_candidate_changes_only_two_year_fields(self): + migrate_seed(self.db_path, self.manifest) + before = {table: self.rows(table) for table in + ('persons', 'titles', 'credits', 'users', 'user_ratings')} + self.add_title_corrections(include_current_source=True) + result = migrate_seed(self.db_path, self.manifest) + self.assertEqual(result['changed_rows'], {'titles': 2}) + self.assertEqual(result['changed_fields'], {'titles.year': 2}) + for table, rows in before.items(): + expected = [row[:-1] + ({3: 2000, 4: 1993}.get(row[0], row[-1]),) + for row in rows] if table == 'titles' else rows + self.assertEqual(self.rows(table), expected) + first = self.db_path.read_bytes() + self.assertEqual(migrate_seed(self.db_path, self.manifest)['changed_rows'], {}) + self.assertEqual(self.db_path.read_bytes(), first) + + def test_unexpected_title_year_is_rejected_without_writes(self): + migrate_seed(self.db_path, self.manifest) + with sqlite3.connect(self.db_path) as connection: + connection.execute("UPDATE titles SET year=2002 WHERE tt_id='tt0209144'") + self.add_title_corrections(include_current_source=True) + before = self.db_path.read_bytes() + with self.assertRaisesRegex(ValueError, 'Unexpected title year'): + migrate_seed(self.db_path, self.manifest) + self.assertEqual(self.db_path.read_bytes(), before) + + +if __name__ == '__main__': + unittest.main() diff --git a/sites/imdb/tests/test_state_tasks.py b/sites/imdb/tests/test_state_tasks.py new file mode 100644 index 00000000..b1100c9d --- /dev/null +++ b/sites/imdb/tests/test_state_tasks.py @@ -0,0 +1,830 @@ +"""Synthetic state-task fixtures, not real browser runs or authenticity evidence. + +Small local SQLite snapshots and params-based browser steps exercise the public +state-task verifier. No Flask server, seed database or old case verifier is used. +""" + +import importlib +import json +from pathlib import Path +import shutil +import sqlite3 +import sys +import tempfile +import unittest + + +SITE = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SITE / "verify")) +verify_lib = importlib.import_module("verify_lib") +state_tasks = importlib.import_module("state_tasks") +RunEvidence = verify_lib.RunEvidence +VerificationError = verify_lib.VerificationError +TASKS = {task["id"]: task for task in ( + json.loads(line) for line in (SITE / "tasks.jsonl").read_text().splitlines() + if line.strip() +)} + +ORBIT = "tt9100001" +CRIME = "tt9100007" +INTERSTELLAR = "tt0816692" +EMAILS = {1: "bob.c@test.com", 2: "carol.d@test.com", 3: "alice.j@test.com", + 4: "other@example.test"} +USERS = {15: 1, 16: 2, 17: 3} +ANSWERS = { + 15: "Alpha Orbit (1990), Sci-Fi, was removed and is no longer in Bob's Watchlist.", + 16: "Alpha Crime (2020) is now rated 8/10 in Carol's My ratings.", + 17: ("Submitted Brilliant sci-fi epic for Interstellar (2014), rated 10/10. " + "The new review appears on Interstellar's user reviews page."), +} + +SCHEMA = """ +CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT, name TEXT, + password_hash TEXT, created_at TEXT); +CREATE TABLE titles (id INTEGER PRIMARY KEY, tt_id TEXT UNIQUE, title_type TEXT, + primary_title TEXT, original_title TEXT DEFAULT '', year INTEGER, + end_year INTEGER, rating_avg REAL DEFAULT 7.0, num_votes INTEGER DEFAULT 100, + runtime_min INTEGER DEFAULT 100, mpaa_rating TEXT DEFAULT 'PG-13'); +CREATE TABLE genres (id INTEGER PRIMARY KEY, name TEXT, slug TEXT); +CREATE TABLE title_genre (title_id INTEGER, genre_id INTEGER, + PRIMARY KEY(title_id,genre_id)); +CREATE TABLE watchlist_items (id INTEGER PRIMARY KEY, user_id INTEGER, + title_id INTEGER, added_at TEXT); +CREATE TABLE user_ratings (id INTEGER PRIMARY KEY, user_id INTEGER, + title_id INTEGER, rating INTEGER, created_at TEXT); +CREATE TABLE reviews (id INTEGER PRIMARY KEY, title_id INTEGER, user_id INTEGER, + rating INTEGER, headline TEXT, body TEXT, helpful_count INTEGER, + created_at TEXT, is_seed INTEGER); +CREATE TABLE news_items (id INTEGER PRIMARY KEY, headline TEXT, summary TEXT, + source TEXT, published_at TEXT, category TEXT, related_tt TEXT); +""" + +TITLES = [ + (1, ORBIT, "tvSeries", "Alpha Orbit", 1990), + (2, "tt9100002", "tvSeries", "Beta Realm", 1990), + (3, "tt9100003", "tvSeries", "Later Both", 2000), + (4, "tt9100004", "tvSeries", "Old Crime Show", 1980), + (5, "tt9100005", "movie", "Earlier Fantasy Movie", 1970), + (6, "tt9100006", "tvSeries", "Unwatched Ancient", 1960), + (7, CRIME, "movie", "Alpha Crime", 2020), + (8, "tt9100008", "movie", "Beta Crime", 2020), + (9, "tt9100009", "movie", "Already Eight", 2023), + (10, "tt9100010", "movie", "Old Crime Film", 2010), + (11, "tt9100011", "tvSeries", "Newest Crime Show", 2024), + (12, "tt9100012", "movie", "Latest Drama Film", 2025), + (13, "tt9100013", "movie", "Outside Crime", 2026), + (14, INTERSTELLAR, "movie", "Interstellar", 2014), +] + + +class SyntheticStateTaskTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.run_dir = Path(self.temp.name) + self.origin = "http://synthetic-state.localhost:48015" + with sqlite3.connect(self.run_dir / "before.db") as db: + db.executescript(SCHEMA) + db.executemany("INSERT INTO users VALUES (?,?,?,?,?)", [ + (user_id, email, {1: "Bob", 2: "Carol", 3: "Alice", 4: "Other"}[user_id], + "synthetic-password-hash", "2024-01-01 00:00:00") + for user_id, email in EMAILS.items() + ]) + db.executemany("INSERT INTO titles (id,tt_id,title_type,primary_title,year) VALUES (?,?,?,?,?)", TITLES) + db.executemany("INSERT INTO genres VALUES (?,?,?)", [ + (1, "Sci-Fi", "sci-fi"), (2, "Fantasy", "fantasy"), + (3, "Crime", "crime"), (4, "Drama", "drama")]) + db.executemany("INSERT INTO title_genre VALUES (?,?)", [ + (1, 1), (2, 2), (3, 1), (3, 2), (4, 3), (5, 2), (6, 1), + (7, 3), (8, 3), (9, 3), (10, 3), (11, 3), (12, 4), + (13, 3), (14, 1), + ]) + db.executemany("INSERT INTO watchlist_items VALUES (?,?,?,?)", [ + (100 + title_id, 1, title_id, "2025-01-01 00:00:00") for title_id in range(1, 6) + ] + [ + (200 + title_id, 2, title_id, "2025-01-01 00:00:00") for title_id in range(7, 13) + ] + [(301, 4, 1, "2025-01-01 00:00:00"), (302, 4, 13, "2025-01-01 00:00:00")]) + db.executemany("INSERT INTO user_ratings VALUES (?,?,?,?,?)", [ + (501, 2, 7, 6, "2025-02-03 04:05:06"), + (502, 2, 9, 8, "2025-02-04 04:05:06"), + (503, 2, 10, 7, "2025-02-05 04:05:06"), + (504, 1, 7, 3, "2025-02-06 04:05:06"), + ]) + db.execute("INSERT INTO reviews VALUES (601,14,3,7,'Existing synthetic review'," + "'An existing body.',4,'2025-03-01 00:00:00',1)") + db.execute("INSERT INTO news_items VALUES (1,'Synthetic bulletin','Keep unchanged'," + "'Fixture','2025-01-01','Synthetic','')") + self.reset_after() + + def reset_after(self): + shutil.copyfile(self.run_dir / "before.db", self.run_dir / "after.db") + + def change(self, sql, parameters=(), both=False): + for filename in (("before.db", "after.db") if both else ("after.db",)): + with sqlite3.connect(self.run_dir / filename) as db: + db.execute(sql, parameters) + + def apply_expected_delta(self, number): + if number == 15: + self.change("DELETE FROM watchlist_items WHERE id=101") + elif number == 16: + self.change("UPDATE user_ratings SET rating=8 WHERE id=501") + elif number == 17: + self.change("INSERT INTO reviews VALUES (602,14,3,10,?,?,?,?,?)", ( + "Brilliant sci-fi epic", "A nonempty synthetic review body.", + 0, "2026-09-08 09:10:11", 0)) + + def step(self, path, action="goto", params=None, after=None, native=False): + step = {"url": self.origin + path, "action": action, "params": params or {}, + "action_result": {"success": None if native else True, "error": None}} + if not native: + step["status"] = "completed" + if after is not None: + step["url_after"] = self.origin + after + return step + + def login_steps(self, user_id, enter=False): + return [ + self.step("/login"), + self.step("/login", "fill", {"selector": "input[name=email]", "value": EMAILS[user_id]}), + self.step("/login", "fill", {"selector": "input[name=password]", "value": "TestPass123!"}), + self.step("/login", "press" if enter else "click", + {"selector": "input[name=password]", "key": "Enter"} if enter + else {"selector": "button[type=submit]", "text": "Sign in"}, after="/"), + self.step("/"), + ] + + def task_steps(self, number, *, title_remove=False, native=False, user_id=None, + enter_login=False, review_sort="helpful", review_entry="title"): + steps = self.login_steps(USERS[number] if user_id is None else user_id, enter_login) + if number == 15: + steps.append(self.step("/list/watchlist")) + source = f"/title/{ORBIT}" if title_remove else "/list/watchlist" + if title_remove: + steps.append(self.step(source)) + params = {"index": 3} if native else { + "selector": f'form[action="/title/{ORBIT}/watchlist"] button', "text": "Remove"} + steps.append(self.step(source, "click", params, after=source, native=native)) + steps.append(self.step(source)) + elif number == 16: + source = f"/title/{CRIME}" + steps += [self.step("/list/watchlist"), self.step(source), + self.step(source, "fill", {"selector": "input[name=rating]", "value": "8"})] + params = {"index": 3} if native else { + "selector": f'form[action="{source}/rate"] button', "text": "Rate"} + steps.append(self.step(source, "click", params, after=source, native=native)) + steps.append(self.step("/list/ratings")) + elif number == 17: + title = f"/title/{INTERSTELLAR}" + form = title + "/review" + if review_entry == "title": + steps.append(self.step(title)) + elif review_entry == "reviews": + steps.append(self.step(title + "/reviews")) + steps += [self.step(form), + self.step(form, "fill", {"selector": "input[name=headline]", "value": "Brilliant sci-fi epic"}), + self.step(form, "fill", {"selector": "textarea[name=body]", "value": "A nonempty synthetic review body."}), + self.step(form, "fill", {"selector": "input[name=rating]", "value": "10"})] + params = {"index": 3} if native else {"selector": "button[type=submit]", "text": "Submit review"} + destination = title + "/reviews?sort=" + review_sort + steps.append(self.step(form, "click", params, after=destination, native=native)) + steps.append(self.step(destination)) + return steps + + def check(self, number, answer=None, steps=None): + task_id = f"IMDb--{number}" + trajectory = {"task_id": task_id, "task": TASKS[task_id]["ques"], + "start_url": self.origin + "/", "fixture_kind": "synthetic", + "final_answer": ANSWERS[number] if answer is None else answer, + "steps": self.task_steps(number) if steps is None else steps} + (self.run_dir / "trajectory.json").write_text(json.dumps(trajectory)) + with RunEvidence(self.run_dir, task_id, expected_ques=TASKS[task_id]["ques"]) as run: + return state_tasks.check_state_task(number, run) + + def assert_accepts(self, number, answer=None, steps=None): + self.assertIsInstance(self.check(number, answer, steps), list) + + def assert_rejects(self, number, answer=None, steps=None): + with self.assertRaises(VerificationError): + self.check(number, answer, steps) + + def test_task15_removes_earliest_qualifying_tv_with_alphabetical_tie_break(self): + self.apply_expected_delta(15) + self.assert_accepts(15) + + def test_task15_target_title_removal_and_confirmation_are_valid(self): + self.apply_expected_delta(15) + self.assert_accepts(15, steps=self.task_steps(15, title_remove=True)) + + def test_task15_nested_watchlist_row_remove_button_is_valid(self): + self.apply_expected_delta(15) + steps = self.task_steps(15) + steps[-2]["params"] = {"selector": "li", "has_link": "Alpha Orbit", + "child": {"role": "button", "name": "Remove"}} + self.assert_accepts(15, steps=steps) + + def test_task15_nested_child_must_describe_the_removal_control(self): + self.apply_expected_delta(15) + for child in ({"role": "button", "name": "Cancel"}, + {"role": "button", "name": "Add to Watchlist"}, + {"role": "link", "name": "Alpha Orbit"}, + {"metadata": {"role": "button", "name": "Remove"}}): + with self.subTest(child=child): + steps = self.task_steps(15) + steps[-2]["params"] = {"selector": "li", "has_link": "Alpha Orbit", "child": child} + self.assert_rejects(15, steps=steps) + + def test_task15_nested_locator_keeps_parent_title_and_origin_constraints(self): + self.apply_expected_delta(15) + variants = ( + {"selector": "li", "has_link": "Beta Realm", "child": {"role": "button", "name": "Remove"}}, + {"selector": "li", "has_link": {"name": "Alpha Orbit"}, "child": {"role": "button", "name": "Remove"}}, + {"selector": 'form[action="/title/tt9100002/watchlist"]', "has_link": "Alpha Orbit", "child": {"role": "button", "name": "Remove"}}, + {"selector": 'form[action="http://foreign.example/title/tt9100001/watchlist"]', "has_link": "Alpha Orbit", "child": {"role": "button", "name": "Remove"}}, + ) + for params in variants: + with self.subTest(params=params): + steps = self.task_steps(15) + steps[-2]["params"] = params + self.assert_rejects(15, steps=steps) + + def test_task15_nested_button_does_not_rescue_noop_or_extra_write(self): + steps = self.task_steps(15) + steps[-2]["params"] = {"selector": "li", "has_link": "Alpha Orbit", + "child": {"role": "button", "name": "Remove"}} + self.assert_rejects(15, steps=steps) + self.apply_expected_delta(15) + self.change("UPDATE news_items SET headline='Unexpected mutation' WHERE id=1") + self.assert_rejects(15, steps=steps) + + def test_control_metadata_outside_locator_child_is_not_an_action_description(self): + self.apply_expected_delta(15) + steps = self.task_steps(15) + steps[-2]["params"] = {"selector": "li", "has_link": "Alpha Orbit", + "metadata": {"role": "button", "name": "Remove"}} + self.assert_rejects(15, steps=steps) + + def test_nested_action_cue_must_describe_child_not_parent_container(self): + self.apply_expected_delta(15) + steps = self.task_steps(15) + steps[-2]["params"] = {"selector": "li", "name": "Remove", "has_link": "Alpha Orbit", + "child": {"role": "link", "name": "Details"}} + self.assert_rejects(15, steps=steps) + + def test_nested_opaque_index_preserves_target_constraints(self): + self.apply_expected_delta(15) + steps = self.task_steps(15, native=True) + steps[-2]["params"] = {"selector": "li", "has_link": "Alpha Orbit", "child": {"index": 3}} + self.assert_accepts(15, steps=steps) + steps[-2]["params"] = {"has_link": "Beta Realm", "child": {"index": 3}} + self.assert_rejects(15, steps=steps) + + def test_task15_either_qualifying_genre_is_sufficient(self): + self.change("UPDATE title_genre SET genre_id=2 WHERE title_id=1", both=True) + self.apply_expected_delta(15) + self.assert_accepts(15, ANSWERS[15].replace("Sci-Fi", "Fantasy")) + + def test_task15_reports_both_qualifying_genres_when_both_present(self): + self.change("INSERT INTO title_genre VALUES (1,2)", both=True) + self.apply_expected_delta(15) + self.assert_accepts(15, ANSWERS[15].replace("Sci-Fi", "Sci-Fi and Fantasy")) + self.assert_rejects(15) + + def test_task15_wrong_tie_choice_type_or_genre_is_rejected(self): + for item_id in (102, 103, 104, 105, 301): + with self.subTest(item_id=item_id): + self.reset_after() + self.change("DELETE FROM watchlist_items WHERE id=?", (item_id,)) + self.assert_rejects(15) + + def test_task15_removing_an_additional_watchlist_item_is_rejected(self): + self.apply_expected_delta(15) + self.change("DELETE FROM watchlist_items WHERE id=102") + self.assert_rejects(15) + + def test_task16_updates_only_existing_target_rating(self): + self.apply_expected_delta(16) + self.assert_accepts(16) + + def test_task16_distinguishes_verified_previous_and_saved_ratings(self): + self.apply_expected_delta(16) + for report in ( + "Updated Carol's personal rating from 6/10 to 8/10", + "Updated Carol's personal rating from 6 to 8", + "The personal rating was 6/10, now 8/10; confirmed on My ratings", + "The personal rating was 6, now 8; confirmed on My ratings", + "It was 6/10. Set the personal rating to 8/10 and confirmed My ratings", + "Previous personal rating: 6/10. Saved personal rating: 8/10", + "Originally rated 6/10; now rated 8/10 and confirmed on My ratings", + "The previous rating was 6/10. The current rating is 8/10, confirmed", + "The saved personal rating was 8/10, confirmed on My ratings", + "It was 6/10. The new personal rating was 8/10, confirmed", + ): + with self.subTest(report=report): + self.assert_accepts(16, "Alpha Crime (2020). " + report + ".") + + def test_task16_real_answer_structure_binds_old_value_to_initial_snapshot(self): + self.change("UPDATE user_ratings SET rating=9 WHERE id=501", both=True) + self.apply_expected_delta(16) + self.assert_accepts(16, "Selected Alpha Crime (2020), the most recently released " + "Crime movie in Carol's initial Watchlist with a personal rating " + "other than 8/10 (it was 9/10). Set Carol's personal rating for " + "Alpha Crime to 8/10 and confirmed the Alpha Crime 2020 row " + "shows 8/10 on My ratings.") + + def test_task16_from_release_year_is_not_a_previous_personal_rating(self): + self.apply_expected_delta(16) + self.assert_accepts(16, "Alpha Crime from 2020. Updated Carol's personal rating " + "from 6 to 8 and confirmed 8/10 on My ratings.") + + def test_task16_rating_transition_is_not_the_catalog_title_from(self): + self.change("INSERT INTO titles(id,tt_id,title_type,primary_title,year) " + "VALUES(15,'tt9100015','tvSeries','From',2022)", both=True) + self.apply_expected_delta(16) + for text in ( + "Alpha Crime (2020): updated Carol's personal rating from 6 to 8. Confirmed My ratings.", + "Alpha Crime (2020): updated Carol's personal rating from 6/10 to 8/10. Confirmed My ratings.", + "Alpha Crime from 2020. Set Carol's personal rating to 8/10, confirmed My ratings.", + ): + with self.subTest(text=text): + self.assert_accepts(16, text) + self.assert_rejects(16, "Alpha Crime (2020): updated Carol's personal rating from 9 to 8. Confirmed My ratings.") + self.assert_rejects(16, "Alpha Crime (2020). From (2022) is now rated 8/10 on My ratings.") + self.assert_rejects(16, "Alpha Crime (2020). From 2022 is now rated 8/10 on My ratings.") + + def test_task16_rating_from_and_real_from_title_keep_separate_bindings(self): + self.change("INSERT INTO titles(id,tt_id,title_type,primary_title,year) " + "VALUES(15,'tt9100015','tvSeries','From',2022)", both=True) + self.apply_expected_delta(16) + with self.subTest(previous="correct"): + self.assert_accepts(16, "Alpha Crime (2020): updated Carol's personal rating from 6 to 8. " + "Confirmed My ratings.\n\nFrom (2022) is a TV series.") + with self.subTest(previous="incorrect"): + self.assert_rejects(16, "Alpha Crime (2020): now rated 8/10, confirmed My ratings; " + "updated the personal rating from 9 to 8.\n\nFrom (2022) is a TV series.") + + def test_task16_incorrect_previous_rating_is_rejected(self): + self.apply_expected_delta(16) + for report in ( + "Updated personal rating from 9/10 to 8/10", + "Updated personal rating from 9 to 8", + "The personal rating was 9/10, now 8/10; confirmed", + "It was 9/10. Set the personal rating to 8/10 and confirmed", + "Previous personal rating: 9/10. Saved personal rating: 8/10", + "Originally rated 9/10; now rated 8/10 and confirmed", + "Previous rating was 6/10. It was 9/10. Now rated 8/10", + ): + with self.subTest(report=report): + self.assert_rejects(16, "Alpha Crime (2020). " + report + ".") + + def test_task16_incorrect_current_rating_is_not_masked_by_history(self): + self.apply_expected_delta(16) + for report in ( + "Updated personal rating from 6/10 to 9/10", + "Updated personal rating from 6 to 9", + "The personal rating was 6/10, now 9/10; confirmed", + "It was 6/10. The current personal rating is 9/10, confirmed", + "It was 6/10. Now rated 8/10, but My ratings shows 9/10", + "It was 6/10. Now rated 8/10, but the current rating is 9", + "The rating was set to 9/10. Now rated 8/10", + "The current personal rating was 6/10. Now rated 8/10", + "It was 6/10. The personal rating is now not 8/10, confirmed", + ): + with self.subTest(report=report): + self.assert_rejects(16, "Alpha Crime (2020). " + report + ".") + + def test_task16_eligibility_or_history_alone_does_not_confirm_eight(self): + self.apply_expected_delta(16) + for report in ( + "Selected a personal rating other than 8/10 (it was 6/10). Confirmed My ratings", + "The previous personal rating was 6/10. Confirmed My ratings", + "Updated the personal rating from 6. Confirmed My ratings", + ): + with self.subTest(report=report): + self.assert_rejects(16, "Alpha Crime (2020). " + report + ".") + + def test_task16_unrated_initial_state_cannot_claim_a_previous_numeric_rating(self): + self.change("DELETE FROM user_ratings WHERE id=501", both=True) + self.change("INSERT INTO user_ratings VALUES (550,2,7,8,'2026-09-08 09:10:11')") + self.assert_rejects(16, "Alpha Crime (2020). It was 6/10. Now rated 8/10.") + self.assert_accepts(16, "Alpha Crime (2020) was unrated. Now rated 8/10 on My ratings.") + + def test_task16_correct_history_cannot_rescue_extra_database_writes(self): + self.apply_expected_delta(16) + self.change("UPDATE news_items SET headline='Unexpected mutation' WHERE id=1") + self.assert_rejects(16, "Alpha Crime (2020). Updated personal rating from 6/10 to 8/10.") + + def test_task16_unrated_target_gets_one_new_rating(self): + self.change("DELETE FROM user_ratings WHERE id=501", both=True) + self.change("INSERT INTO user_ratings VALUES (550,2,7,8,'2026-09-08 09:10:11')") + self.assert_accepts(16) + + def test_task16_null_personal_rating_is_eligible(self): + self.change("UPDATE user_ratings SET rating=NULL WHERE id=501", both=True) + self.apply_expected_delta(16) + self.assert_accepts(16) + + def test_task16_existing_rating_keeps_id_and_created_at(self): + for sql in ("UPDATE user_ratings SET rating=8, id=550 WHERE id=501", + "UPDATE user_ratings SET rating=8, created_at='2026-09-08 09:10:11' WHERE id=501"): + with self.subTest(sql=sql): + self.reset_after() + self.change(sql) + self.assert_rejects(16) + + def test_task16_newer_but_already_eight_and_nonmovie_or_nongenre_are_excluded(self): + for title_id in (8, 9, 10, 11, 12, 13): + with self.subTest(title_id=title_id): + self.reset_after() + self.change("DELETE FROM user_ratings WHERE user_id=2 AND title_id=?", (title_id,)) + self.change("INSERT INTO user_ratings VALUES (550,2,?,8,'2026-09-08 09:10:11')", (title_id,)) + self.assert_rejects(16) + + def test_task16_another_users_rating_or_global_rating_cannot_change(self): + for sql in ("UPDATE user_ratings SET rating=8 WHERE id=504", + "UPDATE titles SET rating_avg=8 WHERE id=7"): + with self.subTest(sql=sql): + self.reset_after() + self.apply_expected_delta(16) + self.change(sql) + self.assert_rejects(16) + + def test_task17_creates_exactly_one_review_without_personal_rating_write(self): + self.apply_expected_delta(17) + self.assert_accepts(17) + + def test_task17_any_review_sort_and_direct_or_reviews_entry_are_valid(self): + self.apply_expected_delta(17) + for order in ("helpful", "recent", "rating"): + for entry in ("title", "direct", "reviews"): + with self.subTest(order=order, entry=entry): + self.assert_accepts(17, steps=self.task_steps(17, review_sort=order, review_entry=entry)) + + def test_task17_created_review_fields_must_match(self): + changes = [("user_id", 4), ("title_id", 7), ("headline", "brilliant sci-fi epic"), + ("headline", "Brilliant sci-fi epic!"), ("rating", 9), + ("body", " \n\t"), ("helpful_count", 1), ("is_seed", 1)] + for field, value in changes: + with self.subTest(field=field, value=value): + self.reset_after() + self.apply_expected_delta(17) + self.change(f"UPDATE reviews SET {field}=? WHERE id=602", (value,)) + self.assert_rejects(17) + + def test_task17_new_review_body_can_be_any_nonempty_text(self): + self.apply_expected_delta(17) + self.change("UPDATE reviews SET body='A different, nonempty review.' WHERE id=602") + steps = self.task_steps(17) + for step in steps: + if step.get("params", {}).get("selector") == "textarea[name=body]": + step["params"]["value"] = "A different, nonempty review." + self.assert_accepts(17, steps=steps) + + def test_task17_extra_review_or_rating_write_is_rejected(self): + for sql in ( + "INSERT INTO reviews SELECT 603,title_id,user_id,rating,headline,body,helpful_count,created_at,is_seed FROM reviews WHERE id=602", + "INSERT INTO user_ratings VALUES (550,3,14,10,'2026-09-08 09:10:11')", + "UPDATE reviews SET body='Changed old review' WHERE id=601", + ): + with self.subTest(sql=sql): + self.reset_after() + self.apply_expected_delta(17) + self.change(sql) + self.assert_rejects(17) + + def test_noop_is_not_success_even_if_answer_and_steps_claim_success(self): + for number in USERS: + with self.subTest(number=number): + self.assert_rejects(number) + + def test_each_task_rejects_changes_to_unrelated_business_tables(self): + for number in USERS: + with self.subTest(number=number): + self.reset_after() + self.apply_expected_delta(number) + self.change("UPDATE news_items SET headline='Unexpected mutation' WHERE id=1") + self.assert_rejects(number) + + def test_correct_delta_requires_login_with_the_requested_account(self): + for number in USERS: + with self.subTest(number=number): + self.reset_after() + self.apply_expected_delta(number) + self.assert_rejects(number, steps=self.task_steps(number, user_id=4)) + self.assert_rejects(number, steps=self.task_steps(number)[5:]) + + def test_enter_can_submit_login(self): + for number in USERS: + with self.subTest(number=number): + self.reset_after() + self.apply_expected_delta(number) + self.assert_accepts(number, steps=self.task_steps(number, enter_login=True)) + + def test_native_index_click_with_unknown_success_uses_later_page_and_delta(self): + for number in USERS: + with self.subTest(number=number): + self.reset_after() + self.apply_expected_delta(number) + steps = self.task_steps(number, native=True) + for step in steps: + step.pop("status", None) + step["action_result"]["success"] = None + self.assert_accepts(number, steps=steps) + + def test_explicit_failed_mutation_is_not_rescued_by_a_plausible_delta(self): + for number in USERS: + with self.subTest(number=number): + self.reset_after() + self.apply_expected_delta(number) + steps = self.task_steps(number) + steps[-2]["status"] = "failed" + steps[-2]["action_result"] = {"success": True, "error": "Synthetic click failed"} + self.assert_rejects(number, steps=steps) + + def test_unknown_after_url_alone_does_not_confirm_mutation(self): + for number in USERS: + with self.subTest(number=number): + self.reset_after() + self.apply_expected_delta(number) + steps = self.task_steps(number, native=True)[:-1] + self.assert_rejects(number, steps=steps) + + def test_explicit_success_after_url_can_confirm_the_review(self): + self.apply_expected_delta(17) + self.assert_accepts(17, steps=self.task_steps(17)[:-1]) + + def test_pure_navigation_does_not_prove_a_mutation_action(self): + for number in USERS: + with self.subTest(number=number): + self.reset_after() + self.apply_expected_delta(number) + steps = self.task_steps(number) + for step in steps[5:]: + step["action"] = "goto" + step["params"] = {} + self.assert_rejects(number, steps=steps) + + def test_foreign_login_and_mutation_urls_do_not_count(self): + for number in USERS: + with self.subTest(number=number): + self.reset_after() + self.apply_expected_delta(number) + steps = self.task_steps(number) + for step in steps: + for key in ("url", "url_after"): + if key in step: + step[key] = step[key].replace(self.origin, "http://foreign.example") + self.assert_rejects(number, steps=steps) + + def test_required_candidate_and_confirmation_pages_cannot_be_omitted(self): + self.apply_expected_delta(15) + # Using a title removal control still requires the initial Watchlist candidate set. + steps = [step for step in self.task_steps(15, title_remove=True) + if not step["url"].endswith("/list/watchlist")] + self.assert_rejects(15, steps=steps) + self.reset_after() + self.apply_expected_delta(16) + steps = [step for step in self.task_steps(16) if not step["url"].endswith("/list/watchlist")] + self.assert_rejects(16, steps=steps) + self.assert_rejects(16, steps=self.task_steps(16)[:-1]) + self.reset_after() + self.apply_expected_delta(17) + steps = [step for step in self.task_steps(17) if not step["url"].endswith("/review")] + self.assert_rejects(17, steps=steps) + + def test_explicit_wrong_target_controls_are_rejected(self): + for number in (15, 16): + with self.subTest(number=number): + self.reset_after() + self.apply_expected_delta(number) + steps = self.task_steps(number) + steps[-2]["params"]["selector"] = 'form[action="/title/tt9100002/watchlist"] button' + self.assert_rejects(number, steps=steps) + + def test_explicit_conflicting_form_values_are_rejected(self): + for number, selector, wrong_value in ( + (16, "input[name=rating]", "7"), + (17, "input[name=rating]", "9"), + (17, "input[name=headline]", "A different headline"), + ): + with self.subTest(number=number, selector=selector): + self.reset_after() + self.apply_expected_delta(number) + steps = self.task_steps(number) + for step in steps: + if step["params"].get("selector") == selector: + step["params"]["value"] = wrong_value + self.assert_rejects(number, steps=steps) + + def test_answers_must_report_the_selected_target_and_result(self): + wrong = {15: "Beta Realm (1990), Fantasy, was removed from Bob's Watchlist.", + 16: "Alpha Crime (2020) is now rated 7/10 in My ratings.", + 17: "Submitted A different headline for Interstellar with rating 10/10."} + for number, answer in wrong.items(): + with self.subTest(number=number): + self.reset_after() + self.apply_expected_delta(number) + self.assert_rejects(number, answer) + self.assert_rejects(number, steps=[]) + + def test_private_confirmation_cannot_belong_to_a_different_account(self): + for number in (15, 16): + with self.subTest(number=number): + self.reset_after() + self.apply_expected_delta(number) + steps = self.task_steps(number) + confirmation = steps.pop() + steps[-1].pop("url_after", None) + steps.extend(self.login_steps(4)) + steps.append(confirmation) + self.assert_rejects(number, steps=steps) + + def test_watchlist_candidate_page_must_belong_to_the_requested_account(self): + for number in (15, 16): + with self.subTest(number=number): + self.reset_after() + self.apply_expected_delta(number) + wrong_watchlist = self.login_steps(4) + [self.step("/list/watchlist")] + rest = [step for step in self.task_steps(number, title_remove=True) + if not step["url"].endswith("/list/watchlist")] + self.assert_rejects(number, steps=wrong_watchlist + rest) + + def test_personal_and_review_ratings_are_distinct_from_imdb_context(self): + self.apply_expected_delta(16) + self.assert_accepts(16, "Alpha Crime (2020): personal rating 8/10; IMDb rating 7.0/10. Confirmed in My ratings.") + self.assert_rejects(16, "Alpha Crime (2020): personal rating 7/10; IMDb rating 8/10. Confirmed in My ratings.") + self.reset_after() + self.apply_expected_delta(17) + self.assert_accepts(17, "Interstellar: my review rating is 10/10; IMDb rating 7.0/10. The review is visible.") + self.assert_rejects(17, "Interstellar: my review rating is 9/10; IMDb rating 10/10. The review is visible.") + + def test_review_confirmation_can_be_concise_but_cannot_claim_a_wrong_headline(self): + self.apply_expected_delta(17) + self.assert_accepts(17, "Confirmed: the new review is visible on the user reviews page.") + self.assert_rejects(17, 'Interstellar review "Wrong headline" is now visible.') + self.assert_rejects(17, 'Interstellar: headline Wrong headline. The new review is visible.') + self.assert_accepts(17, 'Interstellar: headline Brilliant sci-fi epic; the new review is visible.') + + def test_task17_pilot_answer_with_later_headline_reference(self): + # Genuine pilot-01 answer wording, exercised with synthetic DB/UI fixtures. + # The original run and its old verdict are separate, immutable calibration inputs. + self.apply_expected_delta(17) + self.assert_accepts(17, ( + "Signed in as alice.j@test.com (Alice Johnson) and posted one written review " + "for Interstellar (2014). Headline: Brilliant sci-fi epic. Review rating: 10/10. " + "Body: A moving and ambitious science-fiction adventure, with stunning visuals, " + "memorable music, and a powerful story about family and exploration. " + "Confirmed the new review appears on Interstellar’s user reviews page, attributed " + "to Alice Johnson, with the headline and 10/10 rating displayed." + )) + + def test_task17_headline_reference_is_not_an_additional_literal_value(self): + self.apply_expected_delta(17) + for reference in ("with the headline and 10/10 rating displayed", + "with the headline with a 10/10 rating", + "with the headline on the user reviews page"): + for prefix in ("", "Headline: Brilliant sci-fi epic. "): + with self.subTest(reference=reference, prefix=prefix): + self.assert_accepts(17, prefix + "The Interstellar review is visible, " + reference + ".") + + def test_task17_headline_value_can_precede_identifiable_metadata(self): + self.apply_expected_delta(17) + for field in ("Headline: Brilliant sci-fi epic", "Headline is Brilliant sci-fi epic", + "Headline Brilliant sci-fi epic, review rating 10/10", + "Headline: Brilliant sci-fi epic and review rating 10/10", + "Headline: Brilliant sci-fi epic with a rating of 10/10", + "Headline: Brilliant sci-fi epic for Interstellar (2014)", + "Headline: Brilliant sci-fi epic and the new review is visible", + "Headline: Brilliant sci-fi epic (10/10)", + "Headline: Brilliant sci-fi epic, and rating 10/10", + 'Headline: "Brilliant sci-fi epic", rating 10/10'): + with self.subTest(field=field): + self.assert_accepts(17, field + ". Confirmed the Interstellar review is visible.") + + def test_task17_explicit_wrong_headline_cannot_hide_behind_a_reference(self): + self.apply_expected_delta(17) + for field in ("Headline: Wrong headline", "Headline is Wrong headline", + "Headline Wrong headline", 'Headline: "Wrong headline"', + "Headline: and beyond", "Headline: with extras", + "Headline: Brilliant sci-fi epic. Headline: Wrong headline"): + with self.subTest(field=field): + self.assert_rejects(17, field + ". The Interstellar review is visible, " + "with the headline and 10/10 rating displayed.") + + def test_task17_unquoted_extra_headline_suffix_is_not_metadata(self): + self.apply_expected_delta(17) + for suffix in (" extended", " and beyond", " with extras", " for everyone", + " on Mars", ", extended", " (extended)"): + with self.subTest(suffix=suffix): + self.assert_rejects(17, "Headline: Brilliant sci-fi epic" + suffix + + ". Confirmed the Interstellar review is visible.") + + def test_task17_quoted_extra_headline_suffix_is_not_metadata(self): + self.apply_expected_delta(17) + for quotes in (('"', '"'), ("“", "”"), ("'", "'"), ("‘", "’")): + for suffix in (" extended", " and beyond", " with extras", " with a rating of 10/10"): + with self.subTest(quotes=quotes, suffix=suffix): + self.assert_rejects(17, "Headline: " + quotes[0] + "Brilliant sci-fi epic" + + suffix + quotes[1] + ". Confirmed the Interstellar review is visible.") + self.assert_accepts(17, "Headline: " + quotes[0] + "Brilliant sci-fi epic" + + quotes[1] + ". Confirmed the Interstellar review is visible.") + + def test_task17_headline_reference_does_not_override_contradictory_rating(self): + self.apply_expected_delta(17) + for explicit, displayed in ((9, 10), (10, 9), (9, 9)): + with self.subTest(explicit=explicit, displayed=displayed): + self.assert_rejects(17, "Headline: Brilliant sci-fi epic. " + f"Review rating: {explicit}/10. The Interstellar review is visible, " + f"with the headline and {displayed}/10 rating displayed.") + + def test_explicit_cancel_text_does_not_count_as_a_submission(self): + for number in USERS: + with self.subTest(number=number): + self.reset_after() + self.apply_expected_delta(number) + steps = self.task_steps(number) + steps[-2]["params"] = {"text": "Cancel"} + self.assert_rejects(number, steps=steps) + + def test_abandoned_form_inputs_do_not_override_a_reopened_native_form(self): + self.apply_expected_delta(16) + source = f"/title/{CRIME}" + steps = self.task_steps(16, native=True) + position = next(index for index, step in enumerate(steps) if step["params"].get("selector") == "input[name=rating]") + steps[position:position] = [self.step(source, "fill", {"selector": "input[name=rating]", "value": "7"}), + self.step("/list/watchlist"), self.step(source)] + corrected = next(step for step in reversed(steps) if step["action"] == "fill") + corrected["params"] = {"index": 7, "value": "8"} + self.assert_accepts(16, steps=steps) + + def test_last_corrected_form_value_is_used(self): + self.apply_expected_delta(16) + steps = self.task_steps(16) + wrong = self.step(f"/title/{CRIME}", "fill", {"selector": "input[name=rating]", "value": "7"}) + position = next(index for index, step in enumerate(steps) if step["params"].get("selector") == "input[name=rating]") + steps.insert(position, wrong) + self.assert_accepts(16, steps=steps) + + def test_native_unknown_login_and_enter_submission_are_supported(self): + for number in USERS: + with self.subTest(number=number): + self.reset_after() + self.apply_expected_delta(number) + steps = self.task_steps(number, native=True) + for step in steps: + step.pop("status", None) + step["action_result"] = {"success": None, "error": None} + steps[-2]["action"] = "press_key" + steps[-2]["params"] = {"key": "Enter"} + self.assert_accepts(number, steps=steps) + + def test_get_and_head_logout_405_observations_preserve_login(self): + self.apply_expected_delta(16) + for method in ("GET", "HEAD"): + with self.subTest(method=method): + steps = self.task_steps(16) + rejected = self.step("/logout", "goto", {"method": method}, after="/logout") + rejected["action_result"]["status_code"] = 405 + steps[5:5] = [rejected, self.step("/logout", "observe"), self.step("/")] + self.assert_accepts(16, steps=steps) + + def test_obsolete_logout_get_link_does_not_clear_login(self): + self.apply_expected_delta(16) + steps = self.task_steps(16) + rejected = self.step("/", "click", {"selector": 'a[href="/logout"]', + "text": "Sign out", "method": "GET"}, after="/logout") + rejected["action_result"]["status_code"] = 405 + steps[5:5] = [rejected, self.step("/logout", "observe"), self.step("/")] + self.assert_accepts(16, steps=steps) + + def test_post_logout_button_or_enter_clears_the_previous_login(self): + self.apply_expected_delta(16) + for action, selector in ( + ("click", 'form[action="/logout"][method="post"] button'), + ("press_key", 'form[action="/logout"][method="post"] button'), + ("click", "button.nav-link"), + ): + with self.subTest(action=action, selector=selector): + steps = self.task_steps(16) + params = {"selector": selector, + "method": "POST", "text": "Sign out"} + if action == "press_key": + params["key"] = "Enter" + steps[5:5] = [self.step("/", action, params, after="/"), self.step("/")] + self.assert_rejects(16, steps=steps) + + def test_native_unknown_logout_needs_a_later_local_observation(self): + task_id = "IMDb--16" + steps = self.login_steps(2) + [self.step("/", "click", {"text": "Sign out"}, native=True)] + for observed in (False, True): + with self.subTest(observed=observed): + recorded = steps + ([self.step("/")] if observed else []) + trajectory = {"task_id": task_id, "task": TASKS[task_id]["ques"], + "start_url": self.origin + "/", "steps": recorded} + (self.run_dir / "trajectory.json").write_text(json.dumps(trajectory)) + with RunEvidence(self.run_dir, task_id) as run: + self.assertEqual(state_tasks._login_before(run, len(recorded), EMAILS[2]), not observed) + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/imdb/tests/test_verify_entry.py b/sites/imdb/tests/test_verify_entry.py new file mode 100644 index 00000000..addf5dd7 --- /dev/null +++ b/sites/imdb/tests/test_verify_entry.py @@ -0,0 +1,77 @@ +"""Exercise the public subprocess contract, using explicitly synthetic runs.""" + +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +import test_read_tasks as fixtures + + +class VerifierEntryTests(unittest.TestCase): + def setUp(self): + self.fixture = fixtures.SyntheticReadTaskTests("runTest") + self.fixture.setUp() + self.addCleanup(self.fixture.doCleanups) + self.fixture.check(2) + self.run_dir = self.fixture.run_dir + self.other_cwd = tempfile.TemporaryDirectory() + self.addCleanup(self.other_cwd.cleanup) + + def invoke(self, number=2): + result = subprocess.run( + [sys.executable, str(fixtures.SITE / "verify" / f"verify_{number}.py"), + "--run_dir", str(self.run_dir)], + cwd=self.other_cwd.name, capture_output=True, text=True, timeout=20) + self.assertEqual(result.stderr, "") + verdict = json.loads(result.stdout) + self.assertEqual(verdict["task_id"], f"IMDb--{number}") + self.assertIsInstance(verdict["pass"], bool) + return result.returncode, verdict + + def test_only_absolute_run_dir_argument_works_from_another_cwd(self): + code, verdict = self.invoke() + self.assertEqual(code, 0) + self.assertTrue(verdict["pass"]) + + def test_missing_after_snapshot_never_falls_back_to_live_database(self): + (self.run_dir / "after.db").unlink() + code, verdict = self.invoke() + self.assertEqual(code, 1) + self.assertFalse(verdict["pass"]) + self.assertIn("Missing after", verdict["reason"]) + + def test_same_id_old_question_is_not_current_candidate_evidence(self): + path = self.run_dir / "trajectory.json" + trajectory = json.loads(path.read_text()) + trajectory["task"] = "Old question with weaker requirements" + path.write_text(json.dumps(trajectory)) + code, verdict = self.invoke() + self.assertEqual(code, 1) + self.assertFalse(verdict["pass"]) + self.assertIn("question", verdict["reason"]) + + def test_every_task_entry_rejects_another_task_or_missing_artifacts(self): + (self.run_dir / "after.db").unlink() + for task_id in fixtures.TASKS: + number = int(task_id.split("--")[1]) + with self.subTest(task_id=task_id): + code, verdict = self.invoke(number) + self.assertEqual(code, 1) + self.assertFalse(verdict["pass"]) + + def test_current_tasks_and_numbered_entries_are_one_to_one(self): + retained = {0, 2, 7, 9, 10, 12, 14, 15, 16, 17} + expected = retained | set(range(18, 28)) + task_numbers = {int(task_id.split("--")[1]) for task_id in fixtures.TASKS} + entries = {int(path.stem.split("_")[1]) + for path in (fixtures.SITE / "verify").glob("verify_[0-9]*.py")} + self.assertEqual(task_numbers, expected) + self.assertEqual(entries, expected) + self.assertEqual(fixtures.read_tasks.READ_TASKS, retained - {15, 16, 17}) + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/imdb/tests/test_verify_lib.py b/sites/imdb/tests/test_verify_lib.py new file mode 100644 index 00000000..7af9628d --- /dev/null +++ b/sites/imdb/tests/test_verify_lib.py @@ -0,0 +1,285 @@ +"""Synthetic verifier fixtures; these tests are not real browser-run evidence.""" + +import contextlib +import importlib.util +import io +import json +from pathlib import Path +import sqlite3 +import tempfile +import unittest + + +MODULE_PATH = Path(__file__).resolve().parents[1] / "verify" / "verify_lib.py" +SPEC = importlib.util.spec_from_file_location("imdb_verify_lib", MODULE_PATH) +verify_lib = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(verify_lib) +RunEvidence = verify_lib.RunEvidence +VerificationError = verify_lib.VerificationError + + +class SyntheticRunTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.run = Path(self.temp.name) + self.origin = "http://synthetic-audit.localhost:48015" + self.trajectory = { + "task_id": "IMDb--synthetic", + "task": "Open the movie chart.", + "start_url": self.origin + "/", + "final_answer": "Synthetic answer.", + "fixture_kind": "synthetic", + "steps": [], + } + for name in ("before.db", "after.db"): + with sqlite3.connect(self.run / name) as db: + db.executescript( + "CREATE TABLE ratings (id INTEGER PRIMARY KEY, rating INTEGER);" + "INSERT INTO ratings VALUES (1, 7);" + "CREATE TABLE users (id INTEGER, password_hash TEXT);" + "INSERT INTO users VALUES (1, 'synthetic-secret-hash');" + "CREATE TABLE duplicates (value);" + "INSERT INTO duplicates VALUES ('same'), ('same');" + ) + + def load(self, **kwargs): + (self.run / "trajectory.json").write_text(json.dumps(self.trajectory)) + return RunEvidence(self.run, "IMDb--synthetic", **kwargs) + + def step(self, path, after=None, success=True, **extra): + step = {"url": self.origin + path, "action": "click"} + if after is not None: + step["url_after"] = self.origin + after + if success is not None: + step["action_result"] = {"success": success, "error": None} + step.update(extra) + self.trajectory["steps"].append(step) + return step + + def test_local_start_hosts_and_runtime_port_override(self): + for origin in ("http://localhost:48015", "http://127.0.0.1:54321", + "http://[::1]:12345", "http://fresh.case.localhost:48015"): + with self.subTest(origin=origin): + self.trajectory["start_url"] = origin + "/" + self.trajectory["steps"] = [{"url": origin + "/chart/top"}] + with self.load() as run: + self.assertTrue(run.visited("/chart/top")) + + def test_foreign_start_urls_rejected(self): + for url in ("https://localhost:48015/", "http://imdb.com/", + "http://localhost.evil/", "http://localhost@evil/", + "http://user@localhost/", "http://localhost:bad/", + "http://localhost:99999/", "file:///localhost/chart/top", + " http://localhost/", "http://bad..localhost/"): + with self.subTest(url=url): + self.trajectory["start_url"] = url + with self.assertRaises(VerificationError): + self.load() + + def test_foreign_events_same_path_and_embedded_path_do_not_count(self): + self.trajectory["steps"] = [ + {"url": url, "url_after": url, "action_result": {"success": True}} + for url in ( + "http://evil.example/chart/top", + "http://localhost:48015/chart/top", + "http://synthetic-audit.localhost:48016/chart/top", + "https://synthetic-audit.localhost:48015/chart/top", + "http://evil.example/?next=/chart/top", + "http://user@synthetic-audit.localhost:48015/chart/top", + "/chart/top", + ) + ] + with self.load() as run: + self.assertFalse(run.visited("/chart/top")) + self.assertEqual(run.events, []) + + def test_default_http_port_and_hostname_case_are_equivalent(self): + self.trajectory["start_url"] = "http://LOCALHOST/" + self.trajectory["steps"] = [{"url": "http://localhost:80/chart/top"}] + with self.load() as run: + self.assertTrue(run.visited("/chart/top")) + + def test_failed_after_url_does_not_prove_navigation(self): + self.step("/title/A", "/name/B", success=False, status="failed") + with self.load() as run: + self.assertTrue(run.visited("/title/A")) + self.assertFalse(run.visited("/name/B")) + self.assertEqual(run.successful_steps(), []) + self.assertEqual(run.events[0]["phase"], "before") + self.step("/name/B", success=None) + with self.load() as run: + self.assertTrue(run.visited("/name/B")) + + def test_failure_overrides_other_success_flags_and_unknown_is_not_success(self): + self.step("/", "/failed", status="completed", + action_result={"success": True, "error": "failure"}) + self.step("/", "/failed-status", status="failed") + self.step("/", "/unknown", action_result={"success": None, "error": None}) + self.step("/", "/completed", success=None, status="completed") + with self.load() as run: + self.assertFalse(run.visited("/failed")) + self.assertFalse(run.visited("/failed-status")) + self.assertFalse(run.visited("/unknown")) + self.assertTrue(run.visited("/completed")) + self.assertEqual(len(run.successful_steps()), 1) + + def test_malformed_success_status_does_not_crash_or_prove_navigation(self): + self.step("/", "/bad-status", status={"completed": True}) + self.step("/", "/bad-success", status="completed", action_result={"success": "false"}) + with self.load() as run: + self.assertFalse(run.visited("/bad-status")) + self.assertFalse(run.visited("/bad-success")) + self.assertEqual(run.successful_steps(), []) + + def test_tail_slash_query_subsets_and_decoding(self): + url = "/search/title/?genres=crime&genres=drama&tag=a&tag=a&q=dark+knight&blank=" + self.step(url) + with self.load() as run: + self.assertTrue(run.visited("/search/title")) + self.assertTrue(run.visited("/search/title/", {"genres": ["crime"]})) + self.assertTrue(run.visited("/search/title", {"tag": ["a", "a"]})) + self.assertFalse(run.visited("/search/title", {"tag": ["a", "a", "a"]})) + self.assertTrue(run.visited("/search/title", {"q": ["dark knight"], "blank": [""]})) + self.assertFalse(run.visited("/search/title", {"absent": [""]})) + self.assertFalse(run.visited("/SEARCH/title")) + self.assertFalse(run.visited("/search")) + self.assertEqual(run.visit_urls("/search/title"), [self.origin + url]) + + def test_ordered_visits_require_each_event_but_no_implicit_home(self): + self.step("/B", "/A") + with self.load() as run: + self.assertTrue(run.visited("/A")) + self.assertTrue(run.visited("/B")) + self.assertTrue(run.has_ordered_visits(["/B", "/A"])) + self.assertFalse(run.has_ordered_visits(["/A", "/B"])) + self.assertFalse(run.has_ordered_visits(["/B", "/A", "/B"])) + self.assertFalse(run.visited("/")) + + def test_start_url_alone_is_not_visit_evidence(self): + with self.load() as run: + self.assertFalse(run.visited("/")) + + def test_wrong_task_replay_and_question_replay_rejected(self): + self.trajectory["task_id"] = "IMDb--other" + with self.assertRaisesRegex(VerificationError, "task ID"): + self.load() + self.trajectory["task_id"] = "IMDb--synthetic" + with self.assertRaisesRegex(VerificationError, "question"): + self.load(expected_ques="Open the other chart.") + del self.trajectory["task"] + with self.assertRaises(VerificationError): + self.load(expected_ques="Open the movie chart.") + + def test_question_whitespace_only_normalization_and_final_answer(self): + self.trajectory["task"] = " Open\n the movie\tchart. " + with self.load(expected_ques="Open the movie chart.") as run: + self.assertEqual(run.answer, "Synthetic answer.") + self.trajectory["task"] = "Open themovie chart." + with self.assertRaises(VerificationError): + self.load(expected_ques="Open the movie chart.") + + def test_missing_snapshots_fail_without_live_fallback(self): + (self.run / "before.db").unlink() + with self.assertRaisesRegex(VerificationError, "initial"): + self.load() + self.assertFalse((self.run / "before.db").exists()) + (self.run / "after.db").rename(self.run / "initial.db") + with self.assertRaisesRegex(VerificationError, "after"): + self.load() + + def test_initial_filename_and_explicit_overrides(self): + (self.run / "before.db").rename(self.run / "initial.db") + with self.load() as run: + self.assertEqual(run.diff_tables(), {}) + (self.run / "initial.db").rename(self.run / "explicit-before.db") + with self.load(initial_db=self.run / "explicit-before.db") as run: + self.assertEqual(run.diff_tables(), {}) + + def test_connections_are_readonly_rows_and_closed(self): + with self.load() as run: + conn = run.initial + self.assertIsInstance(conn.execute("SELECT * FROM ratings").fetchone(), sqlite3.Row) + with self.assertRaises(sqlite3.OperationalError): + conn.execute("UPDATE ratings SET rating=9") + with self.assertRaises(sqlite3.ProgrammingError): + conn.execute("SELECT 1") + + def test_multiset_change_and_extra_table_write_detected_without_secret_log(self): + with sqlite3.connect(self.run / "after.db") as db: + db.execute("UPDATE ratings SET rating=8") + db.execute("UPDATE users SET password_hash='another-synthetic-hash'") + db.execute("DELETE FROM duplicates WHERE rowid=1") + with self.load() as run: + diff = run.diff_tables() + self.assertEqual(diff["ratings"]["columns"], ["id", "rating"]) + self.assertEqual(diff["ratings"]["before"], [{"id": 1, "rating": 7}]) + self.assertEqual(diff["ratings"]["after"], [{"id": 1, "rating": 8}]) + self.assertEqual(diff["duplicates"]["before"], [{"value": "same"}]) + self.assertEqual(diff["duplicates"]["after"], []) + with self.assertRaises(VerificationError) as error: + run.assert_unchanged(except_tables=("ratings", "duplicates")) + self.assertIn("users", str(error.exception)) + self.assertNotIn("hash", str(error.exception)) + run.assert_unchanged(except_tables=("ratings", "users", "duplicates")) + + def test_reordered_rows_are_unchanged(self): + with sqlite3.connect(self.run / "after.db") as db: + db.execute("DELETE FROM duplicates") + db.executemany("INSERT INTO duplicates VALUES (?)", [("same",), ("same",)]) + with self.load() as run: + run.assert_unchanged() + + def test_empty_business_table_added_removed_or_schema_changed(self): + with sqlite3.connect(self.run / "before.db") as db: + db.execute("CREATE TABLE removed (a TEXT)") + db.execute("CREATE TABLE altered (a TEXT)") + with sqlite3.connect(self.run / "after.db") as db: + db.execute("CREATE TABLE added (a TEXT)") + db.execute("CREATE TABLE altered (a INTEGER)") + with self.load() as run: + diff = run.diff_tables() + self.assertEqual(set(diff), {"removed", "added", "altered"}) + self.assertTrue(all(row["schema_changed"] for row in diff.values())) + with self.assertRaises(VerificationError): + run.assert_unchanged() + + def test_storage_types_and_null_are_not_stringified(self): + with sqlite3.connect(self.run / "before.db") as db: + db.executemany("INSERT INTO duplicates VALUES (?)", [(None,), (1,), (b"1",)]) + with sqlite3.connect(self.run / "after.db") as db: + db.executemany("INSERT INTO duplicates VALUES (?)", [(None,), ("1",), (b"1",)]) + with self.load() as run: + diff = run.diff_tables()["duplicates"] + self.assertEqual(diff["before"], [{"value": 1}]) + self.assertEqual(diff["after"], [{"value": "1"}]) + + def test_sqlite_internal_tables_are_ignored(self): + for name in ("before.db", "after.db"): + with sqlite3.connect(self.run / name) as db: + db.execute("CREATE TABLE counted (id INTEGER PRIMARY KEY AUTOINCREMENT)") + with sqlite3.connect(self.run / "after.db") as db: + db.execute("INSERT INTO counted DEFAULT VALUES") + db.execute("DELETE FROM counted") + with self.load() as run: + run.assert_unchanged() + + def test_emit_result_is_one_json_object_and_exit_status(self): + output = io.StringIO() + with contextlib.redirect_stdout(output): + code = verify_lib.emit_result("IMDb--synthetic", False, "Synthetic failure", ["table changed"]) + self.assertEqual(code, 1) + self.assertEqual(len(output.getvalue().splitlines()), 1) + result = json.loads(output.getvalue()) + self.assertEqual(result, {"task_id": "IMDb--synthetic", "pass": False, + "reason": "Synthetic failure", "evidence": ["table changed"]}) + + def test_cli_arguments(self): + args = verify_lib.parse_args(["--run_dir", str(self.run), "--initial_db", "a.db", "--after_db", "b.db"]) + self.assertEqual(args.run_dir, str(self.run)) + self.assertEqual(args.initial_db, "a.db") + self.assertEqual(args.after_db, "b.db") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/imdb/verify/README.md b/sites/imdb/verify/README.md new file mode 100644 index 00000000..e9fb6818 --- /dev/null +++ b/sites/imdb/verify/README.md @@ -0,0 +1,45 @@ +# IMDb deterministic verification + +The current candidate defines twenty tasks: `0, 2, 7, 9, 10, 12, 14, 15, 16, 17` +and `18–27` (thirteen read-only and seven state-changing tasks). The first ten +contracts are unchanged. New tasks cover constrained film pairs, credit-set +comparisons, displayed financial ratios, account-specific lists, review identity, +release intervals, and source-derived changes to saved lists and ratings. +Implementation is separate from acceptance; the [review report](../../../review-reports/PR-33-IMDB.md) +records execution and independent-review coverage. + +Tasks `1, 3, 4, 5, 6, 8, 11, 13` remain retired after task-quality review. +Historical runs and site features remain available; these IDs have no current +verifier entry. + +Each task row references a `verify_.py` entry point. These use Python's +standard library and read only the supplied, frozen run artifacts: + +```bash +python3 sites/imdb/verify/verify_0.py --run_dir /absolute/path/to/run +``` + +The run directory contains `trajectory.json`, `before.db` (or `initial.db`), +and `after.db`. Explicit `--initial_db` and `--after_db` paths are also accepted. +The trajectory must include the task ID, the current question, a local HTTP +start URL, recorded steps and the final answer. Alternative localhost ports +are supported; navigation must stay on the run's original local origin. + +The command emits one JSON object with `task_id`, `pass`, `reason`, and +`evidence`, returning zero on success and one on failure. The repository's +`agent_demo/eval_judge.py --verifier True` invokes these entries with only +`--run_dir`; no model credentials or live database are needed. + +Expected entities and values are computed from the before snapshot. Read-only +tasks require all business tables to remain unchanged. Stateful tasks permit +only their specified account, target and change, and require corresponding UI +evidence. A correct final answer alone does not demonstrate task execution. +Offline snapshots and trajectories are trusted evaluator inputs, not a defense +against an actor who can fabricate the entire run package. + +Synthetic regression tests exercise failed actions, wrong entities, extra +changes and equivalent answers/paths. They do not count as browser runs: + +```bash +python3 -m unittest discover -s sites/imdb/tests -v +``` diff --git a/sites/imdb/verify/answer_checks.py b/sites/imdb/verify/answer_checks.py new file mode 100644 index 00000000..dcafafaa --- /dev/null +++ b/sites/imdb/verify/answer_checks.py @@ -0,0 +1,178 @@ +"""Small answer checks shared by the IMDb task verifiers. + +Accept prose and ordinary Markdown tables. Monetary answers may use the +displayed precision or the exact catalog value, with equivalent unit names. +Run provenance and screenshot authenticity are checked outside this parser. +""" +from datetime import datetime +from decimal import Decimal +import re +import unicodedata + + +def normalize(text): + text = unicodedata.normalize('NFKD', str(text)) + text = ''.join(c for c in text if not unicodedata.combining(c)).lower() + text = text.replace('’', "'").replace('–', '-').replace('—', '-') + return re.sub(r'[\t ]+', ' ', text.replace('**', '').replace('`', '')).strip() + + +def mentions(text, phrase): + """Whole phrase, without an immediately negated assertion.""" + text, phrase = normalize(text), normalize(phrase) + pattern = r'(?\$|usd\s*)?\s*' + r'(?P\d[\d,]*(?:\.\d+)?)\s*' + r'(?Pbillion|million|thousand|bn|mn|[bmk])?(?!\w)', re.I) +SCALE = {'billion': 10**9, 'bn': 10**9, 'b': 10**9, + 'million': 10**6, 'mn': 10**6, 'm': 10**6, + 'thousand': 10**3, 'k': 10**3} + + +def amount_values(text): + values = [] + for m in AMOUNT.finditer(normalize(text)): + value = Decimal(m['number'].replace(',', '')) + unit = m['unit'] or '' + if m['currency'] or unit or (value >= 1000 and not 1900 <= value <= 2100): + values.append(value * SCALE.get(unit, 1)) + return values + + +def displayed_money(value): + if value is None: + raise ValueError('Missing catalog amount') + for scale in (10**9, 10**6, 10**3): + if value >= scale: + return Decimal(f'{value / scale:.1f}') * scale + return Decimal(value) + + +def has_amount(text, value): + allowed = {Decimal(value), displayed_money(value)} + values = amount_values(text) + return bool(values) and all(v in allowed for v in values) + + +MONEY_FIELDS = { + 'budget': ('production budget', 'budget'), + 'opening': ('opening weekend us & canada', 'opening weekend', 'opening gross', 'opening'), + 'worldwide': ('gross worldwide', 'worldwide gross', 'worldwide', 'global gross'), + 'domestic': ('gross us & canada', 'domestic gross', 'us gross', 'domestic'), +} + + +def money_field(text, field, value, allow_unlabeled=False): + text = normalize(text) + # Ordinary prose also puts the amount before its label. When a clause has + # only one money field, the direction does not change the binding. + clauses = re.split(r'[;|\n]|\band\b', text) + single_field_clauses = [] + for clause in clauses: + present = {key for key, variants in MONEY_FIELDS.items() + if any(re.search(r'(?= nonoverlap[-1][1]: + nonoverlap.append(label) + relevant = [] + for i, (_, end, key) in enumerate(nonoverlap): + if key == field: + stop = nonoverlap[i + 1][0] if i + 1 < len(nonoverlap) else len(text) + relevant.append(text[end:stop]) + if relevant: + return all(has_amount(part, value) for part in relevant) + return allow_unlabeled and not nonoverlap and has_amount(text, value) + + +def has_date(text, date): + date = datetime.strptime(str(date)[:10], '%Y-%m-%d') + text = normalize(text).replace(',', '') + variants = [date.strftime('%Y-%m-%d'), f'{date:%B} {date.day} {date.year}', + f'{date.day} {date:%B} {date.year}', + f'{date:%b} {date.day} {date.year}', f'{date.day} {date:%b} {date.year}'] + return any(mentions(text, variant) for variant in variants) + + +def entity_texts(answer, entities): + """Return text bound to each entity, including row/column Markdown tables. + + entities maps stable keys to visible names and explicitly supported aliases. + A paragraph with several names is split at their mentions; an entity on + its own line retains both prefix and suffix facts from that line. + """ + answer = normalize(answer) + output = {key: [] for key in entities} + patterns = {key: re.compile(r'(? 1: + matches = [(m.start(), m.end(), key) for key, pattern in patterns.items() + for m in pattern.finditer(line)] + matches.sort(key=lambda x: (x[0], -(x[1] - x[0]))) + nonoverlap = [] + for match in matches: + if not nonoverlap or match[0] >= nonoverlap[-1][1]: + nonoverlap.append(match) + for pos, (start, _, key) in enumerate(nonoverlap): + stop = nonoverlap[pos + 1][0] if pos + 1 < len(nonoverlap) else len(line) + output[key].append(line[start:stop]) + return {key: '\n'.join(parts) for key, parts in output.items()} diff --git a/sites/imdb/verify/expansion_account_reads.py b/sites/imdb/verify/expansion_account_reads.py new file mode 100644 index 00000000..60f46e78 --- /dev/null +++ b/sites/imdb/verify/expansion_account_reads.py @@ -0,0 +1,381 @@ +"""Read-only account comparisons; synthetic account state is the source. + +These checks never open a live database or change historical task graders. +Navigation proves access to the relevant account and fact pages; the frozen +snapshots determine membership, attribution and the complete allowed result. +""" + +from collections import defaultdict +from decimal import Decimal +import re + +from answer_checks import entity_texts, has_date, has_number, mentions, normalize +from read_tasks import (_all_directors, _bindings, _names, _one, + _reported_titles, _require, _runtime, _title_path) +from verify_lib import VerificationError + + +ACCOUNT_READ_TASKS = {21, 22} + + +def _account(run, email): + return _one(run.initial, "SELECT * FROM users WHERE email=?", (email,)) + + +def _account_page(run, user, path): + # Shared only by the new task family. Existing 15–17 remain unchanged. + from expansion_state_tasks import authenticated_before + return any(event["path"].rstrip("/") == path + and authenticated_before(run, event["step_index"] + + (event["phase"] == "after"), user["email"]) + for event in run.events) + + +def _source_lists(run, user): + for path in ("/list/watchlist", "/list/ratings"): + _require(_account_page(run, user, path), + "The two source lists were not observed under the required account") + + +def _account_catalog(run, user): + return [dict(row) for row in run.initial.execute( + "SELECT DISTINCT t.* FROM titles t WHERE t.title_type='movie' AND " + "(EXISTS (SELECT 1 FROM watchlist_items w WHERE w.title_id=t.id AND w.user_id=?) " + "OR EXISTS (SELECT 1 FROM user_ratings r WHERE r.title_id=t.id AND r.user_id=?))", + (user["id"], user["id"]))] + + +def _owner_blocks(answer, users): + """Accept owner columns, inline owner labels and ordinary owner sections.""" + aliases = {user["id"]: [user["name"], user["email"], user["name"].split()[0]] + for user in users} + output = entity_texts(answer, aliases) + sections = {key: [] for key in aliases} + active = None + for line in answer.splitlines(): + text = normalize(line).strip("#* -:") + headings = [key for key, names in aliases.items() if any( + re.fullmatch(re.escape(normalize(name)) + + r"(?:'s)?(?:\s+(?:account|movies|films|results|recommendations))?", text) + for name in names)] + if len(headings) == 1: + active = headings[0] + continue + if active is not None: + other = [key for key, names in aliases.items() if key != active + and any(mentions(line, name) for name in names)] + if other: + active = None + else: + sections[active].append(line) + return {key: output[key] + "\n" + "\n".join(sections[key]) for key in aliases} + + +def _language(text): + """Normalize field labels/units only; catalog names remain the evidence keys.""" + text = normalize(text).replace("。", ".").replace(";", ";").replace(",", ",") + text = re.sub(r"(\d{4})年(\d{1,2})月(\d{1,2})日", + lambda m: f"{int(m[1]):04}-{int(m[2]):02}-{int(m[3]):02}", text) + labels = {"重复影评标题": "repeated headline", "重复评论标题": "repeated headline", + "重复标题": "repeated headline", "相同标题": "repeated headline", + "影评标题": "headline", "评论标题": "headline", + "个人评分": "personal rating", "影评评分": "review score", "评论评分": "review score", + "评论日期": "review date", "影评日期": "review date", "发布日期": "review date", + "有帮助人数": "helpful count", "有用票数": "helpful count", "有用数": "helpful count", + "有帮助": "helpful", "作者": "author", "账号": "account", "账户": "account", + "导演": "director", "片长": "runtime", "时长": "runtime", "分钟": " minutes ", + "小时": " hours ", "片名": "film", "电影": "film", "年份": "year", + "日期": "date", "评分": "score"} + for old, new in labels.items(): + text = text.replace(old, " " + new + " ") + return text + + +def _score(text, expected): + text = _language(text) + # Explicit global-score clauses do not describe the requested personal/review score. + number = r"\d+(?:\.\d+)?" + scale = r"(?:\s*(?:/|out of)\s*10)?" + text = re.sub(r"\b(?:imdb|global)\s+(?:rating|score)\s*(?:[:=]|is|of|was)?\s*" + + number + scale, "", text) + text = re.sub(r"满分\s*10\s*分", "", text) + values = re.findall(r"(?=9 " + "AND t.title_type='movie' AND NOT EXISTS (SELECT 1 FROM watchlist_items w " + "WHERE w.user_id=r.user_id AND w.title_id=r.title_id)", (user["id"],))] + _require(bool(selected), "The initial account has no qualifying movie") + text = blocks[user["id"]] + _require(bool(text.strip()), "Results are not attributed to the required account") + bindings = _bindings(text, candidates) + _exclude_extra_titles(text, candidates, {title["id"] for title in selected}) + for title in selected: + facts = bindings[title["id"]] + _require(any(mentions(facts, name) for name in _names(title)), + "A qualifying title is missing or assigned to the wrong account") + _require(_score(facts, title["personal_rating"]), + "The personal score is missing or does not match its owner and movie") + _require(_runtime_units(facts, title["runtime_min"]), + "A qualifying movie's runtime is missing or incorrect") + _require(_all_directors(run, facts, title), + "A qualifying movie's director credits are missing or incorrect") + path = _title_path(title) + _require(run.visited(path) or run.visited(path + "/fullcredits"), + "A reported movie's director facts lack a relevant on-site visit") + return ["Both accounts' own initial ratings-minus-Watchlist sets match the attributed results", + "Personal ratings, runtimes and director credits match; account data is unchanged"] + + +def _review_groups(run, candidates): + groups = defaultdict(list) + for title in candidates: + for row in run.initial.execute( + "SELECT r.*, u.name AS author FROM reviews r JOIN users u ON u.id=r.user_id " + "WHERE r.title_id=?", (title["id"],)): + review = dict(row) + groups[(title["id"], review["headline"])].append(review) + return {key: reviews for key, reviews in groups.items() + if len({review["user_id"] for review in reviews}) > 1} + + +def _helpful(text, expected): + text = normalize(text) + values = re.findall(r"\bhelpful(?:ness)?(?:\s+(?:count|votes))?\s*[:=]?\s*(\d[\d,]*)", text) + # Keep the reverse form on one line. ``\s`` also spans newlines and can + # otherwise misread the day from a preceding ISO date as a helpful count. + values += re.findall(r"(? 0, "A required amount is absent from the snapshot") + # The new questions explicitly calculate from display precision. Passing + # this value to the old helper permits equivalent units, not raw precision. + text = re.sub(r"\bgross us\s*(?:&|and)\s*canada\b", "domestic gross", text) + text = re.sub(r"(^|[;\n])\s*us\s*(?:&|and)\s*canada\s*:", r"\1domestic gross:", text) + return money_field(text, field, int(displayed_money(raw))) + + +def _percent(text, expected): + values = re.findall(r"(? 0, "The percentage denominator must be positive") + return (numerator / denominator * 100).quantize(Decimal("0.1"), rounding=ROUND_HALF_UP) + + +def _winners(lines, expected): + """Recognize an explicit maximum statement or a table Winner=yes column.""" + found = set() + active = False + cue = re.compile(r"\b(?:winners?|highest|largest|greatest|maximum|leaders?)\b|" + r"(?:并列)?(?:最高|最大)(?:为|的是)?") + for line in lines: + if not line.strip(): + active = False + continue + table = re.search(r"\bwinner\s*:\s*(yes|no|true|false)\b", line) + if table: + if table.group(1) in {"yes", "true"}: + found.update(_ids(line)) + continue + if re.fullmatch(r"[ #*-]*(?:joint )?(?:winners?|highest|largest|maximum|leaders?)\s*:", line): + active = True + continue + for clause in re.split(r"\b(?:whereas|while|but|unlike|rather than|compared (?:to|with))\b", line): + if EXCLUSION.search(clause): + continue + if active or cue.search(clause): + ids = _ids(clause) + _require(bool(ids), "A maximum assertion does not identify a catalog movie") + found.update(ids) + if re.match(r"[ #*-]*(?:joint )?(?:winners?|leaders?|maximum)\s*:", clause): + tail = clause.split(":", 1)[1] + tail = TOKEN.sub("", tail) + tail = re.sub(r"\d+(?:\.\d+)?\s*(?:%|percent)?", "", tail) + tail = re.sub(r"\b(?:and|both|all|with|at|a|the|share|percentage|of|tied|are|is)\b", "", tail) + _require(not re.search(r"[a-z]", tail), "An unknown extra winner is asserted") + _require(found == expected, "The explicit maximum set is missing, partial or incorrect") + + +def _amount_rows(run, selected, fields, percentages): + catalog = _catalog(run) + lines, facts = _answer(run, catalog) + _no_extra_results(lines, {t["id"] for t in selected}) + for title in selected: + _details(run, title) + text = facts[title["id"]] + _require(bool(text), "A required movie row is omitted") + for label, column in fields: + _require(_money(text, label, title[column]), "A displayed amount is missing, swapped or bound to the wrong movie") + _require(_percent(text, percentages[title["id"]]), + "A displayed-value percentage is missing or incorrect") + maximum = max(percentages.values()) + _winners(lines, {key for key, value in percentages.items() if value == maximum}) + + +def _check_19(run): + director, directing = _credits(run, "nm0000229", "director") + actor, acting = _credits(run, "nm0000158", "actor") + selected = [title for title in acting if title["id"] in {t["id"] for t in directing}] + _require(bool(selected), "The initial role intersection is empty") + percentages = {} + for title in selected: + percentages[title["id"]] = _percentage(displayed_money(title["box_office_opening"]), + displayed_money(title["budget"])) + _require(_credit_seen(run, director, title, "director") and _credit_seen(run, actor, title, "actor"), + "The shared movie's required credit roles lack relevant local observations") + _amount_rows(run, selected, (("budget", "budget"), ("opening", "box_office_opening")), percentages) + return ["Complete Director/Actor intersection, displayed amounts, percentages and maximum set match"] + + +def _check_20(run): + actor, selected = _credits(run, "nm0000138", "actor") + percentages = {} + for title in selected: + world, domestic = displayed_money(title["box_office_world"]), displayed_money(title["box_office_us"]) + _require(world >= domestic >= 0, "The snapshot's gross values cannot define the requested remainder") + percentages[title["id"]] = _percentage(world - domestic, world) + _require(_credit_seen(run, actor, title, "actor"), "An Actor credit lacks a relevant local observation") + _amount_rows(run, selected, (("domestic", "box_office_us"), ("worldwide", "box_office_world")), percentages) + return ["Complete Actor movie catalog, paired displayed grosses, remainder percentages and all maxima match"] + + +def _number_field(text, labels): + values = [] + pattern = r"\b(?:" + labels + r")\b(?:\s*\([^)]*\))?\s*(?::|=|is|of)?\s*(\d+(?:\.\d+)?)" + values += [Decimal(v) for v in re.findall(pattern, text)] + return values + + +def _runtime(text): + values = [] + hours = re.compile(r"(?= 2 or explicit_pair) and not EXCLUSION.search(line): + if active is not None: + blocks.append(active) + active = line + elif active is not None and not ids and line.strip(): + active += "; " + line + else: + if active is not None: + blocks.append(active) + active = None + if active is not None: + blocks.append(active) + return blocks + + +def _double_feature_text(answer): + """Normalize equivalent Task 18 fields, without translating film identity.""" + text = normalize(answer).replace("。", "\n") + # Chinese prose has no word boundary before/after an English title or unit. + text = re.sub(r"(?<=[a-z0-9])(?=[\u3400-\u9fff])|(?<=[\u3400-\u9fff])(?=[a-z0-9])", " ", text) + text = re.sub(r"(\d+(?:\.\d+)?)\s*分钟", r"\1 minutes", text) + text = re.sub(r"(?:评分合计|评分总和|评分之和|评分总计|总评分)\s*[::]?", "summed rating ", text) + text = re.sub(r"(?:合计|总时长|时长合计)\s*[::]?\s*(?=\d+(?:\.\d+)?\s*minutes)", "combined runtime ", text) + text = re.sub(r"\bimdb\s*(?=\d)", "imdb rating ", text) + text = text.replace("不符合", " excluded ") + return text + + +def _double_feature_blocks(lines): + """Bind separate totals only when the answer identifies exactly one pair. + + A two-row table and its following totals already identify a program; the + question does not require a repeated same-line pair label. With several + programs, retain explicit pair-local binding and reject ambiguous totals. + """ + relevant = [line for line in lines if not EXCLUSION.search(line)] + blocks = _pair_blocks(relevant) + members = set(key for line in relevant for key in _ids(line)) + if len(members) == 2 and all(set(_ids(block)) == members for block in blocks): + return ["; ".join(relevant)] + return blocks + + +def _check_18(run): + eligible = [dict(row) for row in run.initial.execute( + "SELECT DISTINCT t.* FROM titles t JOIN title_genre tg ON tg.title_id=t.id " + "JOIN genres g ON g.id=tg.genre_id WHERE g.slug='animation' AND t.title_type='movie' " + "AND t.year<=2010 AND t.mpaa_rating IN ('G','PG') AND t.runtime_min IS NOT NULL " + "AND t.rating_avg IS NOT NULL")] + feasible = [(a, b) for a, b in combinations(eligible, 2) if a["runtime_min"] + b["runtime_min"] <= 190] + _require(bool(feasible), "The snapshot has no feasible double feature") + rating = lambda t: Decimal(f'{t["rating_avg"]:.1f}') + maximum = max(rating(a) + rating(b) for a, b in feasible) + optimal = {frozenset((a["id"], b["id"])): (a, b) for a, b in feasible if rating(a) + rating(b) == maximum} + selected_ids = set().union(*optimal) + selected = [t for t in eligible if t["id"] in selected_ids] + lines, facts = _answer(run, _catalog(run), _double_feature_text(run.answer)) + _no_extra_results(lines, selected_ids) + for title in selected: + _details(run, title) + text = "; ".join(clause for clause in re.split(r"[;\n]", facts[title["id"]]) + if not re.search(r"\b(?:total|combined|summed|sum|together)\b", clause)) + scores = _number_field(text, r"(?:imdb )?rating|score") + scores += [Decimal(v) for v in re.findall(r"(?= 2, "A release timeline needs at least two movies") + minimum = min((dates[b["id"]] - dates[a["id"]]).days for a, b in zip(ordered, ordered[1:])) + lines, facts = _answer(run, _catalog(run)) + _no_extra_results(lines, set(dates)) + timeline_lines = [line for line in lines if not re.search(r"\b(?:closest|shortest|minimum|gap|interval|pair)\b", line)] + aliases = {title["id"]: ["filmkey" + str(title["id"])] for title in titles} + timeline_facts = entity_texts(_paragraph_answer("\n".join(timeline_lines), aliases), aliases) + sequence = [] + for title in titles: + _details(run, title) + _require(_credit_seen(run, actor, title, "actor"), "An Actor timeline credit lacks a local observation") + # Timeline rows provide date ownership. Pair summaries need not repeat + # dates already bound to those exact titles in the complete timeline. + row_dates = _dates(timeline_facts[title["id"]]) + _require(bool(row_dates) and all(d == dates[title["id"]] for d in row_dates), + "A movie's full release date is absent, conflicting or misattributed") + for line in timeline_lines: + if _dates(line): + sequence += [key for key in _ids(line) if key in dates and key not in sequence] + _require(set(sequence) == set(dates) and [dates[i] for i in sequence] == sorted(dates.values()), + "The complete movie timeline is not chronological") + # Same-date films can appear in either order. Adjacency follows the valid + # reported timeline, rather than imposing an unrequested ID tie-breaker. + expected = {frozenset((a, b)) for a, b in zip(sequence, sequence[1:]) + if (dates[b] - dates[a]).days == minimum} + reported = set() + for block in _pair_blocks(lines): + if not re.search(r"\b(?:days?|gap|interval|closest|shortest|minimum)\b", block): + continue + pair = frozenset(_ids(block)) + _require(pair in expected, "A non-minimal or extra release pair is asserted") + values = [int(v) for v in re.findall(r"(? 512 or password_size > 4096: + return False + return _verify_password(encoded, password) + + +def _submitted_password_matches(user, recorded): + """Accept the recorder's exact safe placeholder when the snapshot agrees. + + Authentication still requires a successful same-origin transition away + from the login/register page. For a redacted field, validate the account's + stored hash against the task-supplied password instead of treating the + placeholder itself as a credential. + """ + if verify_password(user["password_hash"], recorded): + return True + return (recorded in {"[SUPPLIED PASSWORD]", "[REDACTED]"} + and verify_password(user["password_hash"], PASSWORD)) + + +@lru_cache(maxsize=64) +def _verify_password(encoded, password): + try: + method, salt, digest = encoded.split("$") + if not re.fullmatch(r"[A-Za-z0-9]{1,64}", salt): + return False + if not re.fullmatch(r"[0-9a-f]+", digest): + return False + parameters = method.split(":") + if parameters[0] == "scrypt" and len(parameters) == 4: + n, r, p = map(int, parameters[1:]) + if (n < 2 or n & (n - 1) or n > 32768 or not 1 <= r <= 8 + or not 1 <= p <= 4 or n * r * p > 1048576 or len(digest) != 128): + return False + actual = hashlib.scrypt(password.encode(), salt=salt.encode(), n=n, r=r, p=p, + dklen=64, maxmem=64 * 1024 * 1024).hex() + elif parameters[0] == "pbkdf2" and len(parameters) == 3: + algorithm, iterations = parameters[1], int(parameters[2]) + if algorithm not in {"sha256", "sha512"} or not 1 <= iterations <= 2000000: + return False + if len(digest) != {"sha256": 64, "sha512": 128}[algorithm]: + return False + actual = hashlib.pbkdf2_hmac(algorithm, password.encode(), salt.encode(), iterations).hex() + else: + return False + return hmac.compare_digest(actual, digest) + except (ValueError, TypeError, OverflowError, MemoryError): + return False + + +def _observed_destination(run, index): + """Only this action's valid after URL or the immediate next before URL.""" + raw = run.trajectory["steps"][index].get("url_after") + if raw is not None: + parsed = _local_url(raw) + if not parsed or parsed[1] != run._origin: + return None + for phase, position in (("after", index), ("before", index + 1)): + events = [e for e in run.events if e["step_index"] == position and e["phase"] == phase] + if events: + return events[0]["path"].rstrip("/") or "/" + return None + + +def _form_values(run, index, path): + values = {} + for step in run.trajectory["steps"][:index]: + parsed = _local_url(step.get("url")) + if not parsed or parsed[1] != run._origin or parsed[0].path.rstrip("/") != path: + values.clear() + continue + if not _action_ok(step): + continue + action = normalize(step.get("action", "")).replace("_", "") + if action in {"goto", "navigate", "reload", "refresh", "goback"}: + values.clear() + if action not in {"fill", "input", "type", "inputtext", "select", "selectoption", "fillform"}: + continue + params = _params(step) + value = params.get("value", params.get("text", params.get("values"))) + if isinstance(value, list) and len(value) == 1: + value = value[0] + if not isinstance(value, str): + continue + control = _control_text(step) + field = next((f for f in ("email", "password", "name") + if re.search(r"\b" + f + r"\b", control)), None) + # Native DOM indices may omit field labels; recognize only these direct + # input values, never arbitrary recursive metadata or locator ancestors. + if field is None: + if re.fullmatch(r"[^\s@]+@[^\s@]+\.[^\s@]+", value.strip()): + field = "email" + elif value == PASSWORD: + field = "password" + elif value == "Weekend Viewer": + field = "name" + if field: + values[field] = value if field == "password" else value.strip() + if "email" in values: + values["email"] = values["email"].lower() + return values + + +def _control(run, step, operation, title=None): + if not _action_ok(step) or not _interaction(step): + return False + result = step.get("action_result") or {} + if str(result.get("status_code", "")).startswith(("4", "5")): + return False + nodes = list(_locator_nodes(step)) + text = _control_text(step, include_ancestors=True) + for address in re.findall(r"https?://[^\s\"'\]]+", text): + parsed = _local_url(address) + if not parsed or parsed[1] != run._origin: + return False + if title: + for node in nodes: + if "has_link" in node and normalize(node["has_link"]) != normalize(title["primary_title"]): + return False + for tt_id, endpoint in re.findall(r"/title/([^/\s\"'\]?]+)/(watchlist|rate|review)\b", text): + if tt_id != title["tt_id"] or endpoint != ("watchlist" if operation == "add" else operation): + return False + leaf = _control_text(step) + if re.search(r"\b(cancel|remove|delete|details)\b", leaf) or "−" in leaf: + return False + if operation == "logout": + if normalize(_params(step).get("method", "post")) != "post": + return False + elif re.search(r"\blog\s*out\b|\bsign out\b", leaf): + return False + if not leaf: + if operation == "logout": + return False + index = nodes[-1].get("index") + return type(index) is int and index >= 0 + if normalize(nodes[-1].get("role", "")) == "link" or re.match(r"a\[", leaf): + return False + patterns = {"add": r"add to watchlist|\+\s*watchlist|\bwatchlist\b", + "rate": r"\brate\b|\brating\b", "login": r"sign in|log ?in", + "register": r"create (?:your )?(?:imdb )?account|register|sign up", + "logout": r"log ?out|sign out"} + if re.search(patterns[operation], leaf): + if operation == "add" and re.search(r"(?:^|\s)-\s*watchlist", leaf): + return False + return True + if operation == "logout": + # A generic submit (including a rating save) is never a logout. + return False + # Form submit and Enter are valid without a human-readable button label. + return (normalize(step.get("action", "")) in {"submit", "press", "presskey", "press_key", "keypress"} + or bool(re.search(r"button\[type=.?(?:submit)", leaf))) + + +class _Authentication: + def __init__(self, run): + self.before, self.after, self.transitions = [], [], [] + users = {row["email"].lower(): dict(row) for row in run.initial.execute("SELECT * FROM users")} + initial_emails = set(users) + after_users = {row["email"].lower(): dict(row) for row in run.after.execute("SELECT * FROM users")} + active = None + before_events = {e["step_index"]: e for e in run.events if e["phase"] == "before"} + for i, step in enumerate(run.trajectory["steps"]): + self.before.append(active) + event = before_events.get(i) + destination = _observed_destination(run, i) + if event and destination and _action_ok(step): + path = event["path"].rstrip("/") + logout = _control(run, step, "logout") or _opaque_logout(run, i) + if active and logout and destination in {"/", "/login"}: + self.transitions.append((i, "logout", active)) + active = None + elif active is None and path in {"/login", "/register"} and destination not in {"/login", "/register", "/logout"}: + operation = "login" if path == "/login" else "register" + if _control(run, step, operation): + fields = _form_values(run, i, path) + email = fields.get("email") + user = users.get(email) if operation == "login" else after_users.get(email) + if (operation == "register" and (email in initial_emails or not user + or fields.get("name") != user["name"])): + user = None + if user and _submitted_password_matches(user, fields.get("password")): + if operation == "register": + users[email] = user + active = email + self.transitions.append((i, operation, email)) + self.after.append(active) + + def owner(self, event): + return (self.before if event["phase"] == "before" else self.after)[event["step_index"]] + + +def _opaque_logout(run, index): + """Corroborate an indexed logout through the app's guarded auth forms. + + A numeric click alone is insufficient. It must leave for home and the next + distinct observed route must be login/register, which this app will not + render for an authenticated user. Known GET/HEAD and error outcomes fail. + This remains dependent on authentic page observations, like indexed writes. + """ + step = run.trajectory["steps"][index] + leaf = list(_locator_nodes(step))[-1] + ancestors = _control_text(step, include_ancestors=True) + for address in re.findall(r"https?://[^\s\"'\]]+", ancestors): + parsed = _local_url(address) + if not parsed or parsed[1] != run._origin: + return False + for target in re.findall(r"action=[\"']([^\"']+)", ancestors): + if target != "/logout" and not (target.startswith("http://") and _local_url(target) + and _local_url(target)[0].path == "/logout"): + return False + if (_control_text(step) or type(leaf.get("index")) is not int or leaf["index"] < 0 + or not _action_ok(step) or not _interaction(step) + or normalize(_params(step).get("method", "post")) != "post" + or str((step.get("action_result") or {}).get("status_code", "")).startswith(("4", "5")) + or _observed_destination(run, index) not in {"/", "/login", "/register"}): + return False + for event in run.events: + if event["step_index"] <= index: + continue + path = event["path"].rstrip("/") or "/" + if path != "/": + return path in {"/login", "/register"} + return False + + +def _authentication(run): + if not hasattr(run, "_expansion_authentication"): + run._expansion_authentication = _Authentication(run) + return run._expansion_authentication + + +def authenticated_before(run, position, email): + """Identity after steps[:position]; an after event uses step_index + 1.""" + if not isinstance(position, int) or not 0 <= position <= len(run.trajectory["steps"]): + return False + auth = _authentication(run) + return position > 0 and auth.after[position - 1] == email.lower() + + +def _pages(run, email, paths, before=None, after=None): + auth = _authentication(run) + return [e for e in run.events if e["path"].rstrip("/") in paths + and (email is None or auth.owner(e) == email) + and (before is None or 2 * e["step_index"] + (e["phase"] == "after") < 2 * before) + and (after is None or 2 * e["step_index"] + (e["phase"] == "after") > 2 * after)] + + +def _rows(db, sql, parameters=()): + return [dict(r) for r in db.execute(sql, parameters)] + + +def _user(run, email): + users = _rows(run.initial, "SELECT * FROM users WHERE email=?", (email,)) + _require(len(users) == 1, "Initial snapshot does not identify the required user") + return users[0] + + +def _title_path(title): + return "/title/" + title["tt_id"] + + +def _schema(db): + return sorted(tuple(r) for r in db.execute( + "SELECT type,name,tbl_name,sql FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%' ORDER BY type,name")) + + +def _insertions(run, expected): + _require(_schema(run.initial) == _schema(run.after), "Business schema or indexes changed") + changes = run.diff_tables() + wanted = {table for table, rows in expected.items() if rows} + _require(set(changes) == wanted, "Database changed outside the exact allowed insertion set") + for table, specifications in expected.items(): + if not specifications: + continue + change = changes[table] + _require(not change["schema_changed"] and not change["before"], "Existing business rows changed or disappeared") + timestamp = "added_at" if table == "watchlist_items" else "created_at" + supplied = [] + for row in change["after"]: + _require(type(row.get("id")) is int and row["id"] > 0, "Invalid new row identity") + try: + datetime.fromisoformat(row[timestamp]) + except (ValueError, TypeError, KeyError): + raise VerificationError("Invalid new row timestamp") from None + expected_fields = set(specifications[0]) | {"id", timestamp} + _require(set(row) == expected_fields, "Unexpected fields in a new row") + supplied.append(tuple(sorted((key, row[key]) for key in specifications[0]))) + wanted_rows = [tuple(sorted(row.items())) for row in specifications] + _require(Counter(supplied) == Counter(wanted_rows), "New rows do not match the required complete user/title/value set") + + +def _write(run, user, title, operation, used=None): + auth = _authentication(run) + used = used if used is not None else set() + for event in run.events: + i = event["step_index"] + path = event["path"].rstrip("/") + if event["phase"] != "before" or i in used or auth.before[i] != user["email"]: + continue + if path != _title_path(title) and not (operation == "add" and path in { + "/chart/top", "/chart/toptv", "/chart/moviemeter", "/chart/boxoffice"}): + continue + step = run.trajectory["steps"][i] + if not _control(run, step, operation, title): + continue + if not _pages(run, user["email"], {"/list/watchlist" if operation == "add" else "/list/ratings"}, after=i): + continue + used.add(i) + return i + raise VerificationError("Missing local authenticated action and subsequent owned-list confirmation") + + +def _answer_texts(run, titles): + # Preserve common category prefixes before the next entity; use the existing + # calibrated title/table bindings for the remaining prose and continuations. + answer = re.sub(r"\b((?:was|is|were)(?:\s+also)?\s+)already saved\b(?!\s*\(\d{4}\))", + r"\1already present", run.answer, flags=re.I) + answer = answer.replace(",", ", ").replace("。", "\n") + answer = re.sub(r"(\d+(?:\.\d+)?)\s*分钟", r"\1 minutes", answer) + answer = re.sub(r";\s*|(?=\b(?:Already saved|Already present|Added|Existing)\s*:)", "\n", answer) + class AnswerView: + pass + view = AnswerView() + view.initial, view.answer = run.initial, answer + return _task_texts(view, titles) + + +def _report(run, titles, runtime=False): + _require(bool(run.answer.strip()), "The answer is empty") + texts = _answer_texts(run, titles) + for title in titles: + text = texts[title["id"]] + _require(text and has_number(text, title["year"]), "The answer omits or misbinds a required title/year") + if runtime: + _require(_runtime(text, title["runtime_min"]), "The answer omits or misstates a runtime") + return texts + + +def _watchlist_ids(run, user): + return {r[0] for r in run.initial.execute("SELECT title_id FROM watchlist_items WHERE user_id=?", (user["id"],))} + + +def _watchlist_movies(run, user): + return _rows(run.initial, "SELECT t.* FROM titles t JOIN watchlist_items w ON w.title_id=t.id " + "WHERE w.user_id=? AND t.title_type='movie'", (user["id"],)) + + +def _saved_rows(user, titles): + return [{"user_id": user["id"], "title_id": t["id"]} for t in titles] + + +def _filtered_search(run): + for event in run.events: + if event["path"].rstrip("/") != "/search/title": + continue + query = event["query"] + try: + correct = query.get("title_type") == ["movie"] and query.get("genre") == ["sci-fi"] + correct &= all(query.get(key) and all(convert(v) == wanted for v in query[key]) + for key, wanted, convert in (("year_from", 1960, int), ("year_to", 1989, int), + ("rating_min", Decimal(8), Decimal))) + if correct: + return True + except (InvalidOperation, ValueError): + pass + return False + + +def _type_evidence(run, title, before): + if _pages(run, None, {_title_path(title)}, before=before): + return True + chart = "/chart/top" if title["title_type"] == "movie" else "/chart/toptv" + if title.get("top_rank") is not None and _pages(run, None, {chart}, before=before): + return True + # Confirm that this title is actually in a type-filtered result set, not + # merely that some unrelated Movie query was opened. + genres = {} + for row in run.initial.execute("SELECT tg.title_id,g.slug FROM title_genre tg JOIN genres g ON g.id=tg.genre_id"): + genres.setdefault(row[0], set()).add(row[1]) + catalog = _rows(run.initial, "SELECT * FROM titles WHERE title_type=?", (title["title_type"],)) + for event in _pages(run, None, {"/search/title"}, before=before): + q = event["query"] + if q.get("title_type") != [title["title_type"]]: + continue + try: + if any(len(set(q.get(key, []))) > 1 for key in ("year_from", "year_to", "rating_min", "sort")): + continue + lo, hi = [int(q.get(key, [""])[0]) if q.get(key, [""])[0] else None + for key in ("year_from", "year_to")] + minimum = Decimal(q.get("rating_min", ["0"])[0] or "0") + if not minimum.is_finite() or any(v is not None and not 1 <= v <= 9999 for v in (lo, hi)): + continue + wanted_genres = q.get("genre", []) + if len(set(wanted_genres)) != len(wanted_genres): + continue + matches = [t for t in catalog if (lo is None or t["year"] is not None and t["year"] >= lo) + and (hi is None or t["year"] is not None and t["year"] <= hi) + and (not minimum or t["rating_avg"] is not None and Decimal(str(t["rating_avg"])) >= minimum) + and set(wanted_genres) <= genres.get(t["id"], set())] + sort = q.get("sort", ["popularity"])[0] + field = {"rating": "rating_avg", "votes": "num_votes", "year": "year", "box_office": "box_office_world"}.get(sort) + if field: + matches.sort(key=lambda t: (t.get(field) is not None, t.get(field) or 0), reverse=True) + else: + matches.sort(key=lambda t: (t.get("popularity_rank") is None, t.get("popularity_rank") or 0)) + if title["id"] in {t["id"] for t in matches[:100]}: + return True + except (ValueError, TypeError, InvalidOperation): + continue + return False + + +def _check24(run): + user = _user(run, "david.k@test.com") + titles = _rows(run.initial, "SELECT DISTINCT t.* FROM titles t JOIN title_genre tg ON tg.title_id=t.id " + "JOIN genres g ON g.id=tg.genre_id WHERE t.title_type='movie' AND g.slug='sci-fi' " + "AND t.year BETWEEN 1960 AND 1989 AND t.rating_avg>=8 AND t.runtime_min<=120") + titles = [t for t in titles if t["id"] not in _watchlist_ids(run, user)] + _require(titles, "Initial catalog has no missing qualifying movies") + _require(_filtered_search(run), "Required Advanced search constraints were not observed") + _insertions(run, {"watchlist_items": _saved_rows(user, titles)}) + used = set() + first = min(_write(run, user, t, "add", used) for t in titles) + _require(_pages(run, user["email"], {"/list/watchlist"}, before=first), "Missing initial David Watchlist evidence") + _report(run, titles, runtime=True) + return ["Complete filtered movie set added to David's Watchlist; existing rows and other tables preserved"] + + +def _check25(run): + bob, david = _user(run, "bob.c@test.com"), _user(run, "david.k@test.com") + rated = _rows(run.initial, "SELECT t.* FROM titles t JOIN user_ratings r ON r.title_id=t.id " + "WHERE r.user_id=? AND r.rating=10", (bob["id"],)) + movies = [t for t in rated if t["title_type"] == "movie"] + saved = _watchlist_ids(run, david) + added, existing = [t for t in movies if t["id"] not in saved], [t for t in movies if t["id"] in saved] + _require(added, "Initial data has no missing recommendations") + _insertions(run, {"watchlist_items": _saved_rows(david, added)}) + used = set() + first = min(_write(run, david, t, "add", used) for t in added) + _require(_pages(run, bob["email"], {"/list/ratings"}, before=first), "Bob's personal-rating source was not observed") + _require(_pages(run, david["email"], {"/list/watchlist"}, before=first), "David's initial Watchlist was not observed") + # Title types can also be distinguished through type-filtered search or + # movie/TV charts. No universal detail-page detour is imposed. + for title in rated: + _require(_type_evidence(run, title, first), "Missing on-site movie versus TV type evidence") + texts = _report(run, movies) + for title in added: + text = texts[title["id"]] + contradictory = re.search(r"\b(?:was |is |were )?already (?:saved|present|in)\b|\bexisting (?:entry|item|recommendation)\b", text) + if contradictory: + prefix = text[max(0, contradictory.start() - 15):contradictory.start()] + _require(re.search(r"\b(?:not|wasn't|isn't)\s*$", prefix), + "The answer labels a newly added recommendation as already present") + for title in existing: + text = texts[title["id"]] + # Remove the title itself before looking for status: a title may happen + # to contain 'Already Saved', as in the adversarial synthetic fixture. + name = re.escape(normalize(title["primary_title"])) + text = re.sub(name + r"(?=\s*(?:\(\d{4}\)|\|))", " ", text) + _require(re.search(r"\balready\b|\b(?:existing|kept|unchanged|previously saved)\b", text), + "The answer does not identify the already-saved recommendation") + _require(not re.search(r"\bnewly (?:added|saved)\b", text), "The answer says an existing recommendation was newly saved") + for claim in re.finditer(r"\b(?:added|saved|copied)\s+(?:it\s+)?again\b|\bre-?added\b", text): + prefix = text[max(0, claim.start() - 25):claim.start()] + _require(re.search(r"\b(?:not|never|didn't|did not|wasn't|was not)\s*$", prefix), + "The answer says an existing recommendation was added again") + return ["Bob's movie recommendations deduplicated into David's owned Watchlist; source state preserved"] + + +def _check26(run): + alice = _user(run, "alice.j@test.com") + rated = {r[0] for r in run.initial.execute("SELECT title_id FROM user_ratings WHERE user_id=?", (alice["id"],))} + titles = [t for t in _watchlist_movies(run, alice) if t["id"] not in rated] + _require(titles, "Initial source queue is empty") + _require(not run.initial.execute("SELECT 1 FROM users WHERE email=?", (NEW_EMAIL,)).fetchone(), "New account already existed initially") + users = _rows(run.after, "SELECT * FROM users WHERE email=?", (NEW_EMAIL,)) + _require(len(users) == 1, "The requested new account was not created exactly once") + user = users[0] + _require(user["name"] == "Weekend Viewer" and verify_password(user["password_hash"], PASSWORD), + "New account name or working credentials do not match") + _insertions(run, {"users": [{k: user[k] for k in ("email", "name", "password_hash")}], + "watchlist_items": _saved_rows(user, titles)}) + auth = _authentication(run) + registrations = [i for i, op, email in auth.transitions if op == "register" and email == NEW_EMAIL] + _require(len(registrations) == 1, "Missing successful registration provenance") + registered = registrations[0] + for path in ("/list/watchlist", "/list/ratings"): + _require(_pages(run, alice["email"], {path}, before=registered), "Missing Alice source-set evidence") + used = set() + for title in titles: + _require(_write(run, user, title, "add", used) > registered, "Save predates new-account registration") + relogins = [i for i, op, email in auth.transitions if op == "login" and email == NEW_EMAIL and i > registered + and any(max(used) < j < i and action == "logout" and owner == NEW_EMAIL + for j, action, owner in auth.transitions)] + _require(any(_pages(run, NEW_EMAIL, {"/list/watchlist"}, after=max(i, max(used))) for i in relogins), + "Missing real new-account logout/relogin and final owned Watchlist confirmation") + _report(run, titles) + return ["One new account has valid requested credentials and exactly the source-derived queue; reauthentication observed"] + + +def _check27(run): + alice = _user(run, "alice.j@test.com") + ratings = {r[0] for r in run.initial.execute("SELECT title_id FROM user_ratings WHERE user_id=?", (alice["id"],))} + candidates = [] + for title in _watchlist_movies(run, alice): + if title["id"] in ratings: + continue + reviews = _rows(run.initial, "SELECT * FROM reviews WHERE user_id=? AND title_id=? AND rating IS NOT NULL", + (alice["id"], title["id"])) + candidates.extend((title, review) for review in reviews) + _require(len(candidates) == 1, "Initial data does not identify one movie and one scored Alice review") + title, review = candidates[0] + score = review["rating"] + _require(type(score) is int and 1 <= score <= 10, "Source review score is invalid") + _insertions(run, {"user_ratings": [{"user_id": alice["id"], "title_id": title["id"], "rating": score}]}) + index = _write(run, alice, title, "rate") + _require(_recorded_fields_agree(run, index, _title_path(title), {"rating": score}), "Recorded personal score contradicts source review") + for path in ("/list/watchlist", "/list/ratings"): + _require(_pages(run, alice["email"], {path}, before=index), "Missing initial saved-versus-rated evidence") + source_paths = {_title_path(title) + "/reviews"} + featured = run.initial.execute("SELECT id FROM reviews WHERE title_id=? " + "ORDER BY is_seed DESC,helpful_count DESC LIMIT 3", (title["id"],)).fetchall() + if review["id"] in {r[0] for r in featured}: + source_paths.add(_title_path(title)) + _require(_pages(run, None, source_paths, before=index), "Missing visible source-review page") + text = _report(run, [title])[title["id"]] + _require(mentions(text, review["headline"]), "The answer omits the source review headline") + explicit = _reported_review_headlines(text, title["primary_title"]) + _require(all(h == normalize(review["headline"]) for h in explicit), "The answer reports a different source headline") + text = normalize(text) + for user in _rows(run.initial, "SELECT id,name FROM users WHERE id!=?", (alice["id"],)): + name = re.escape(normalize(user["name"])) + _require(not re.search(r"\b(?:by|author\s*[:=]?)\s+" + name + r"\b|\b" + name + r"'s review\b", text), + "The answer attributes the source review to another author") + global_score = re.compile(r"\b(?:imdb|global)\s+(?:rating|score)\s*(?:is|of|:|=)?\s*(\d+(?:\.\d+)?)(?:\s*/\s*10)?") + for match in global_score.finditer(text): + _require(Decimal(match[1]) == Decimal(str(title["rating_avg"])), "The answer contradicts the displayed global score") + text = global_score.sub(" ", text) + fractions = re.findall(r"(? 2 and mentions(text, surname) + + +def _all_directors(run, text, title): + directors = _directors(run, title) + if not all(_person_mentioned(text, person) for person in directors): + return False + # Only explicit director assertions restrict additional names. Mentioning + # another filmmaker in a comparison or a negative explanation is valid. + people = [dict(row) for row in run.initial.execute("SELECT id, name FROM persons")] + expected = {person["id"] for person in directors} + clauses = re.split(r"[;\n]|(?<=[.!?])\s+", normalize(text)) + for clause in clauses: + assertion = re.search(r"\b(?:directed by|directors?\s*(?:are|is|:|=))\s*(.*)", clause) + if not assertion: + continue + names = re.split(r"\b(?:while|whereas|but|unlike|rather than|compared (?:to|with)|" + r"worldwide|budget|opening|rating)\b", assertion.group(1))[0] + matches = [(match.start(), match.end(), person) for person in people + for match in re.finditer(r"(?= result[-1][1]: + result.append(hit) + return result + + claims = [] + blocks = re.split(r"[\n;]|(?<=[.!?])\s+(?=[a-z])|,\s+(?!\d)|\b(?:while|whereas|but)\b", source) + entity_pattern = "|".join(re.escape(name) for names in entities.values() for name in names) + blocks = [part for block in blocks for part in re.split(r"\band\s+(?=(?:" + entity_pattern + r")\b)", block)] + # Table cells under named entity columns become entity-bound metric lines. + bound = entity_texts(source, entities) + for key, block in bound.items(): + for line in block.splitlines(): + if re.search(METRICS[metric], line) and re.search(r"\b(?:winner|wins|higher|larger|longer)\b", line): + if not hits(line): + blocks.append(entities[key][0] + " " + line) + table_has_winner_header = any("|" in line and re.search(r"\b(?:winner|higher|longer|larger)\b", line) + for line in source.splitlines()) + for block in blocks: + relevant = re.search(METRICS[metric], block) is not None + both = metric in {"rating", "worldwide"} and re.search(r"\b(?:wins? (?:on )?both|higher in both|both comparisons|wins? both categories)\b", block) + if not relevant and not both and not single_metric: + continue + found = hits(block) + negated = bool(re.search(r"\b(?:not|isn't|is not|neither)\b", block)) + tie = re.search(r"\b(?:tied?|equal|same)\b", block) + if tie and not re.search(r"\b(?:higher|larger|longer|lower|smaller|shorter)\b", block): + claims.append((negated, "tie")) + continue + comparisons = list(re.finditer(r"\b(higher|larger|longer|greater|highest|largest|longest|lower|smaller|shorter|wins?|winner|more)\b", block)) + subject = None + for index, comparative in enumerate(comparisons): + prefix = block[comparisons[index - 1].end() if index else 0:comparative.start()] + suffix = block[comparative.end():comparisons[index + 1].start() if index + 1 < len(comparisons) else len(block)] + if comparative.group() in {"highest", "largest", "longest"} and re.search( + r"\b(?:below|behind|lower than|smaller than|shorter than)\s+(?:the\s+)?$", prefix): + continue + applicable = bool(re.search(METRICS[metric], comparative.group() + suffix)) + if not applicable and not any(re.search(pattern, suffix) for pattern in METRICS.values()): + applicable = bool(re.search(METRICS[metric], prefix)) + if not found: + continue + before = [hit for hit in found if hit[1] <= comparative.start()] + after = [hit for hit in found if hit[0] >= comparative.end()] + if comparative.group() in {"winner", "wins", "win"} and after and not before: + subject = after[0][2] + elif subject is None: + subject = before[-1][2] if before else after[0][2] if after else None + if not (applicable or both or single_metric): + continue + winner = subject + if comparative.group() in {"lower", "smaller", "shorter"}: + others = [key for key in values if key != subject] + winner = others[0] if len(others) == 1 else None + if winner is not None: + claims.append((negated, winner)) + if not comparisons and table_has_winner_header and "|" in block and len({hit[2] for hit in found}) == 1: + claims.append((negated, found[0][2])) + expected = "tie" if tied else expected + return any(not negative for negative, _ in claims) and all( + (claim != expected if negative else claim == expected) for negative, claim in claims) + + +def _query_value(query, key, default=None, converter=str): + values = query.get(key) + if values is None: + return default + try: + parsed = [converter(value) for value in values] + except (ValueError, InvalidOperation): + return object() + if not parsed or any(value != parsed[0] for value in parsed): + return object() + return parsed[0] + + +def _advanced_query(query, genre, years=None): + if _query_value(query, "title_type") != "movie" or _query_value(query, "sort") != "rating": + return False + if query.get("genre") != [genre]: + return False + if _query_value(query, "rating_min", converter=Decimal) != Decimal("8.5"): + return False + for index, key in enumerate(("year_from", "year_to")): + if years is None: + if query.get(key) not in (None, [""]): + return False + elif _query_value(query, key, converter=int) != years[index]: + return False + return True + + +def _advanced_visit(run, genre, years=None): + return any(_advanced_query(parse_qs(urlsplit(url).query, keep_blank_values=True), genre, years) + for url in run.visit_urls("/search/title")) + + +def _genre_titles(run, genre, limit=None): + sql = ("SELECT DISTINCT t.* FROM titles t JOIN title_genre tg ON tg.title_id=t.id " + "JOIN genres g ON g.id=tg.genre_id WHERE g.slug=? ORDER BY t.rating_avg DESC") + if limit is not None: + sql += " LIMIT " + str(int(limit)) + return [dict(row) for row in run.initial.execute(sql, (genre,))] + + +def _highest(titles): + usable = [title for title in titles if title["rating_avg"] is not None] + _require(bool(usable), "No rated titles satisfy the required candidate set") + best = max(title["rating_avg"] for title in usable) + return [title for title in usable if title["rating_avg"] == best] + + +def _report_year_rating(run, titles): + texts = _task_texts(run, titles) + for title in titles: + text = texts[title["id"]] + _require(text and mentions(text, title["primary_title"]), "A required movie or series title is missing") + _require(title["year"] is not None and has_number(text, title["year"]), "A title's release/start year is missing or incorrect") + _require(_rating(text, title["rating_avg"]), "A title's IMDb rating is missing or incorrect") + + +def _advanced_result_ids(run, query): + """Evaluate the visible search result set, including its 100-row limit.""" + clauses, parameters = [], [] + first = lambda key, default="": query.get(key, [default])[0] + if first("title_type"): + clauses.append("t.title_type=?") + parameters.append(first("title_type")) + for key, column, operator, convert in (("year_from", "year", ">=", int), + ("year_to", "year", "<=", int), + ("rating_min", "rating_avg", ">=", float)): + try: + value = convert(first(key)) + except (ValueError, TypeError): + continue + if value: + clauses.append(f"t.{column}{operator}?") + parameters.append(value) + genres = query.get("genre", []) + if genres: + slots = ",".join("?" for _ in genres) + clauses.append("t.id IN (SELECT tg.title_id FROM title_genre tg JOIN genres g ON g.id=tg.genre_id " + f"WHERE g.slug IN ({slots}) GROUP BY tg.title_id HAVING COUNT(g.id)=?)") + parameters.extend(genres) + parameters.append(len(genres)) + order = {"rating": "t.rating_avg DESC", "votes": "t.num_votes DESC", "year": "t.year DESC", + "box_office": "t.box_office_world DESC"}.get(first("sort"), "t.popularity_rank ASC NULLS LAST") + sql = "SELECT t.id FROM titles t" + if clauses: + sql += " WHERE " + " AND ".join(clauses) + return {row[0] for row in run.initial.execute(sql + " ORDER BY " + order + " LIMIT 100", parameters)} + + +def _search_result_ids(run, query): + """Mirror the existing public title-search ranking without importing Flask.""" + stop_words = set("the a an in on at to for of and or is it by with this that from about into as be are was were".split()) + tokens = [word for word in re.split(r"\W+", query.lower()) if len(word) > 1 and word not in stop_words] + if not tokens: + return set() + hits = [] + for row in run.initial.execute("SELECT * FROM titles"): + title = dict(row) + genres = " ".join(row[0] for row in run.initial.execute( + "SELECT g.name FROM genres g JOIN title_genre tg ON tg.genre_id=g.id WHERE tg.title_id=?", (title["id"],))) + people = " ".join(row[0] for row in run.initial.execute( + "SELECT p.name FROM credits c JOIN persons p ON p.id=c.person_id WHERE c.title_id=? ORDER BY c.id LIMIT 8", (title["id"],))) + text = " ".join((title["primary_title"], title.get("original_title") or "", str(title["year"] or ""), + genres, people, title.get("plot_short") or "")).lower() + score = sum(token in text for token in tokens) + if score: + hits.append((score, title.get("num_votes") or 0, title["id"])) + hits.sort(key=lambda hit: (-hit[0], -hit[1])) + return {hit[2] for hit in hits[:50]} + + +def _rating_page_seen(run, title): + if run.visited(_title_path(title)): + return True + if title.get("top_rank") is not None and run.visited("/chart/top" if title["title_type"] == "movie" else "/chart/toptv"): + return True + if run.visited("/"): + homepage_queries = ( + "SELECT id FROM titles WHERE top_rank IS NOT NULL ORDER BY top_rank LIMIT 10", + "SELECT id FROM titles WHERE popularity_rank IS NOT NULL ORDER BY popularity_rank LIMIT 8", + "SELECT id FROM titles WHERE year IN (2024,2025,2026) ORDER BY year DESC, rating_avg DESC LIMIT 8", + ) + if any(title["id"] in {row[0] for row in run.initial.execute(sql)} for sql in homepage_queries): + return True + for path, column, limit, descending in (("/chart/boxoffice", "box_office_us", 50, True), + ("/chart/moviemeter", "popularity_rank", 100, False)): + if run.visited(path): + ids = {row[0] for row in run.initial.execute( + f"SELECT id FROM titles WHERE {column} IS NOT NULL ORDER BY {column} {'DESC' if descending else 'ASC'} LIMIT ?", (limit,))} + if title["id"] in ids: + return True + for event in run.events: + path, query = event["path"].rstrip("/"), event["query"] + if path.startswith("/genre/"): + if any(row["id"] == title["id"] for row in _genre_titles(run, path.split("/")[-1], 60)): + return True + if path.startswith("/name/"): + row = run.initial.execute("SELECT known_for_json FROM persons WHERE nm_id=?", (path.split("/")[-1],)).fetchone() + if row: + try: + known = json.loads(row[0] or "[]") + except (ValueError, TypeError): + known = [] + if title["tt_id"] in known: + return True + if path in {"/find", "/search"} and _query_value(query, "s", "all") in {"all", "tt"}: + search = query.get("q", [""])[0] + if title["id"] in _search_result_ids(run, search): + return True + if path == "/search/title": + if title["id"] in _advanced_result_ids(run, query): + return True + return False + + +def _check_0(run): + _require(run.visited("/chart/top"), "The Top 250 Movies chart was not visited") + titles = [_one(run.initial, "SELECT * FROM titles WHERE title_type='movie' AND top_rank=?", (rank,)) for rank in (1, 3)] + texts = _task_texts(run, titles) + difference = abs(titles[0]["runtime_min"] - titles[1]["runtime_min"]) + for rank, title in zip((1, 3), titles): + _require(run.visited(_title_path(title)), "A ranked movie's title information was not visited") + text = texts[title["id"]] + _require(_runtime(text, title["runtime_min"], difference), "A ranked movie's runtime or explicit difference is missing or incorrect") + _require(title["mpaa_rating"] and mentions(text, title["mpaa_rating"]), "A ranked movie's MPAA classification is missing or incorrect") + reported_ranks = re.findall(r"(?:\brank(?:ed)?\s*(?::|=|is)?\s*(?:#|no\.?\s*)?|#)\s*(\d+)\b", normalize(text)) + word_ranks = re.findall(r"\b(first|third)[- ]ranked\b", normalize(text)) + reported_ranks += [{"first": "1", "third": "3"}[word] for word in word_ranks] + _require(all(int(value) == rank for value in reported_ranks), "A movie is bound to the wrong chart rank") + _require(_comparison(run.answer, {t["id"]: _names(t) for t in titles}, + {t["id"]: t["runtime_min"] for t in titles}, "runtime"), "The longer-runtime comparison is missing or incorrect") + return ["Movie chart and both title pages visited; runtime, MPAA and comparison match"] + + +def _check_2(run): + _require(run.visited("/chart/boxoffice"), "The catalog domestic box-office chart was not visited") + title = _one(run.initial, "SELECT * FROM titles WHERE box_office_us IS NOT NULL ORDER BY box_office_us DESC LIMIT 1") + _require(title["title_type"] == "movie", "The first catalog chart entry is not a movie") + _require(run.visited(_title_path(title)), "The chart winner's title page was not visited") + _require(_money(_task_texts(run, [title])[title["id"]], "budget", title["budget"], True), "The chart winner's production budget is missing or incorrect") + return ["Catalog cumulative-domestic chart winner and title-page budget match"] + + +def _check_7(run): + person = _one(run.initial, "SELECT * FROM persons WHERE name=?", ("Christopher Nolan",)) + _require(run.visited("/name/" + person["nm_id"]), "Christopher Nolan's Director filmography was not visited") + titles = [dict(row) for row in run.initial.execute( + "SELECT DISTINCT t.* FROM titles t JOIN credits c ON c.title_id=t.id " + "WHERE c.person_id=? AND c.role='director' AND t.title_type='movie'", (person["id"],))] + best = _highest(titles) + _report_year_rating(run, best) + catalog = [dict(row) for row in run.initial.execute("SELECT * FROM titles")] + mentioned = _reported_titles(run.answer, catalog) + texts = _task_texts(run, best) + for title in mentioned: + if title["id"] in {row["id"] for row in best}: + continue + text = texts[title["id"]] + numeric_rating = re.search(r"\brating\s*(?:[:=]|is|of|at)?\s*\d|\d(?:\.\d+)?\s*/\s*10", text) + if numeric_rating: + _require(_rating(text, title["rating_avg"]), "An additional movie or series is assigned an incorrect rating") + clauses = re.split(r"[;\n]|\b(?:but|while|whereas)\b", text) + extra_winner = any(re.search(r"\b(?:tied?|highest|winner)\b", clause) + and not re.search(r"\b(?:not|isn't|is not|doesn't|does not)\b", clause) + for clause in clauses) + _require(not extra_winner, "An extra title is incorrectly reported as a highest-rating tie") + _require(all(_rating_page_seen(run, title) for title in best), "A winning movie's rating lacks a relevant on-site listing or detail visit") + return ["Director filmography used; all highest-rated movie ties include title, year and rating"] + + +def _reported_titles(answer, titles): + """Recognize catalog title names in answer order, resolving prefix titles.""" + text = normalize(answer) + matches = [] + for title in titles: + for name in _names(title): + for match in re.finditer(r"(?= 8.5] + _require(len(candidates) >= 3, "Fewer than three titles match the task in the initial snapshot") + catalog = [dict(row) for row in run.initial.execute("SELECT * FROM titles")] + selected = _reported_titles(run.answer, catalog) + _require(len(selected) == 3, "The answer must identify exactly three catalog titles") + eligible = {title["id"] for title in candidates} + _require(all(title["id"] in eligible for title in selected), "A reported title does not match the required filters") + cutoff = candidates[2]["rating_avg"] + mandatory = {title["id"] for title in candidates if title["rating_avg"] > cutoff} + _require(mandatory <= {title["id"] for title in selected} and all(title["rating_avg"] >= cutoff for title in selected), + "The answer omits a title above the third-place cutoff or includes a lower-rated title") + _require([title["rating_avg"] for title in selected] == sorted((title["rating_avg"] for title in selected), reverse=True), + "The three titles are not in descending rating order") + return ["Specified Advanced search submitted; three titles satisfy rank order and cutoff-tie policy"] + + +def _check_10(run): + _require(_advanced_visit(run, "crime", (1990, 1999)), "Advanced search lacks the specified Movie, Crime, inclusive years, minimum rating or rating-sort constraints") + candidates = [title for title in _genre_titles(run, "crime") + if title["title_type"] == "movie" and title["year"] is not None and 1990 <= title["year"] <= 1999 + and title["rating_avg"] is not None and title["rating_avg"] >= 8.5] + best = _highest(candidates) + _no_extra_winners(run, best) + texts = _task_texts(run, best) + for title in best: + _require(run.visited(_title_path(title)), "A highest-rated result's title page containing worldwide gross was not visited") + text = texts[title["id"]] + _require(_all_directors(run, text, title), "A highest-rated movie's director credit is missing or incorrectly bound") + _require(_money(text, "worldwide", title["box_office_world"], True), "A highest-rated movie's worldwide gross is missing or incorrectly bound") + return ["All Advanced search constraints observed; all highest-rating ties bind directors and worldwide gross"] + + +def _check_12(run): + _require(run.visited("/genre/crime"), "The Crime genre page was not visited") + candidates = _genre_titles(run, "crime", 60) + movies = _highest([title for title in candidates if title["title_type"] == "movie"]) + series = _highest([title for title in candidates if title["title_type"] == "tvSeries"]) + _no_extra_winners(run, movies + series) + _report_year_rating(run, movies + series) + texts = _task_texts(run, movies + series) + for titles, pattern in ((movies, r"\b(?:movies?|films?)\b"), (series, r"\b(?:tv|television|series|shows?)\b")): + _require(all(re.search(pattern, texts[title["id"]]) for title in titles), "The highest-rated titles are not identified as movie versus TV series") + entities = {"movie": ["movie", "movies", "film", "films"] + [name for title in movies for name in _names(title)], + "series": ["TV", "TV series", "television", "series", "show"] + [name for title in series for name in _names(title)]} + # These colon labels identify winners within each already-checked group; + # they do not assert that both groups beat the other group's top rating. + comparison_answer = re.sub( + r"\b(?:highest[- ]rated|top(?:[- ]rated)?)\s+(?:crime\s+)?" + r"(?=(?:movies?|films?|tv[- ]series|television(?: series)?|series|shows?)\s*:)", + "", normalize(run.answer)) + _require(_comparison(comparison_answer, entities, {"movie": movies[0]["rating_avg"], "series": series[0]["rating_avg"]}, "rating", single_metric=True), + "The movie-versus-series top-rating comparison is missing or incorrect") + return ["Crime genre visited; both type groups, complete top ties, years, ratings and group comparison match"] + + +def _check_14(run): + titles = [_named_title(run, name, year) for name, year in (("The Dark Knight", 2008), ("Inception", 2010))] + texts = _task_texts(run, titles) + for title in titles: + _require(run.visited(_title_path(title)), "One compared movie's title page was not visited") + text = texts[title["id"]] + _require(_rating(text, title["rating_avg"]), "A compared movie's rating is missing or incorrectly bound") + _require(_money(text, "worldwide", title["box_office_world"]), "A compared movie's worldwide gross is missing or incorrectly bound") + entities = {title["id"]: _names(title) for title in titles} + for metric, field in (("rating", "rating_avg"), ("worldwide", "box_office_world")): + _require(_comparison(run.answer, entities, {title["id"]: title[field] for title in titles}, metric), + "The " + metric + " comparison is missing or incorrect") + return ["Both movie pages visited; ratings and worldwide grosses bind correctly and both comparisons match"] + + +def check_read_task(number, run): + """Return short evidence descriptions, or raise VerificationError.""" + _require(number in READ_TASKS, "Unsupported read-only task number") + _require(bool(run.answer.strip()), "The run has no final answer") + run.assert_unchanged() + try: + result = globals()["_check_" + str(number)](run) + except (sqlite3.Error, KeyError, TypeError, ValueError, InvalidOperation) as error: + raise VerificationError("The supplied snapshot or answer lacks required task data") from error + return result + ["All business tables are unchanged"] diff --git a/sites/imdb/verify/state_tasks.py b/sites/imdb/verify/state_tasks.py new file mode 100644 index 00000000..e41b2fe9 --- /dev/null +++ b/sites/imdb/verify/state_tasks.py @@ -0,0 +1,502 @@ +"""Offline checks for IMDb tasks 15–17, using exact snapshot differences. + +Indexed native clicks do not identify their DOM control in trajectory.json. +For those actions, a local source page, a nonfailed action, the exact database +change and a subsequent local observation are checked together. Screenshot +authenticity and the causal integrity of an evidence package remain external +review responsibilities; a success flag or answer alone never proves a write. +""" + +from datetime import datetime +import re +import sqlite3 +from urllib.parse import urlsplit + +from answer_checks import entity_texts, has_number, mentions, normalize +from verify_lib import VerificationError + + +STATE_TASKS = {15, 16, 17} +FAILURE_STATES = {"failed", "error", "interrupted", "cancelled", "canceled", "pending", "started"} + + +def _require(condition, reason): + if not condition: + raise VerificationError(reason) + + +def _row(db, sql, parameters): + rows = db.execute(sql, parameters).fetchall() + _require(len(rows) == 1, "The initial snapshot does not identify one required entity") + return dict(rows[0]) + + +def _user(run, email): + return _row(run.initial, "SELECT * FROM users WHERE email=?", (email,)) + + +def _title_path(title): + return "/title/" + title["tt_id"] + + +def _strings(value): + if isinstance(value, str): + yield value + elif isinstance(value, dict): + for part in value.values(): + yield from _strings(part) + elif isinstance(value, (list, tuple)): + for part in value: + yield from _strings(part) + + +def _params(step): + return step.get("params") if isinstance(step.get("params"), dict) else {} + + +def _action_ok(step): + """Native success=None is unknown, but can be corroborated by state/pages.""" + status = step.get("status") + if status is not None and (not isinstance(status, str) or status in FAILURE_STATES): + return False + result = step.get("action_result") + if result is not None and not isinstance(result, dict): + return False + result = result or {} + if step.get("error") or result.get("error") or result.get("success") is False: + return False + if result.get("success") is not None and not isinstance(result["success"], bool): + return False + return isinstance(step.get("action_result"), dict) or status == "completed" + + +def _locator_nodes(step): + """Only the recorder's explicit child chain describes nested controls.""" + node = _params(step) + while isinstance(node, dict): + yield node + node = node.get("child") + + +def _control_text(step, include_ancestors=False): + keys = ("selector", "locator", "target", "button", "role", "name", "label") + if normalize(step.get("action", "")) in {"click", "submit"}: + keys += ("text",) + nodes = list(_locator_nodes(step)) + if not include_ancestors: + nodes = nodes[-1:] + return normalize(" ".join(part for node in nodes + for key in keys for part in _strings(node.get(key)))) + + +def _interaction(step): + action = normalize(step.get("action", "")).replace("_", "") + if action in {"click", "submit"}: + return True + if action in {"press", "presskey", "presskeys", "keypress", "key"}: + return any(re.search(r"\b(?:enter|return)\b", normalize(text)) for text in _strings(_params(step))) + return False + + +def _control_matches(run, step, operation, title=None): + """Check explicit control descriptions; native DOM indices stay valid.""" + text = _control_text(step, include_ancestors=True) + for address in re.findall(r"https?://[^\s\"'\]]+", text): + try: + source, target = urlsplit(run.trajectory["start_url"]), urlsplit(address) + if (target.scheme, target.hostname, target.port or 80) != (source.scheme, source.hostname, source.port or 80): + return False + except ValueError: + return False + if title: + for node in _locator_nodes(step): + if "has_link" in node and (not isinstance(node["has_link"], str) + or normalize(node["has_link"]) != normalize(title["primary_title"])): + return False + explicit = re.findall(r"/title/([^/\s\"'\]?]+)/(?:watchlist|rate|review)\b", text) + if any(tt_id != title["tt_id"] for tt_id in explicit): + return False + # Ancestors constrain origin/row identity; only the acted-on child names + # the action. A parent container saying Remove cannot turn Details into it. + ancestors_described = bool(text) + text = _control_text(step) + if not text: + index = list(_locator_nodes(step))[-1].get("index") + return not ancestors_described or (type(index) is int and index >= 0) + if re.search(r"\b(?:cancel|add to watchlist|logout|log out|sign out)\b", text): + return False + action = normalize(step.get("action", "")) + if action in {"press", "press_key", "press_keys", "keypress", "key", "submit"}: + return True + if re.search(r"\b(?:combobox|textbox|textarea|select)\b", text) and "button" not in text: + return False + if re.search(r"(?:^|\s)(?:input|a)\[", text) and not re.search(r"button|submit", text): + return False + patterns = {"login": r"sign in|log ?in|submit", "remove": r"remove|watchlist|submit", + "rate": r"rate|rating|submit", "review": r"post|review|submit"} + return bool(re.search(patterns[operation], text)) + + +def _before_events(run): + return {event["step_index"]: event for event in run.events if event["phase"] == "before"} + + +def _logout_observed(run, position, index, step): + """A POST logout control can end a login; viewing its URL cannot.""" + control = _control_text(step) + if not _interaction(step) or not re.search(r"\b(?:logout|log out|sign out)\b", control): + return False + method = normalize(_params(step).get("method", "")) + if method and method != "post": + return False + if re.search(r"(?:^|\s)a\[", control): + return False + result = step.get("action_result") or {} + if str(result.get("status_code", "")).startswith(("4", "5")): + return False + if result.get("success") is True or step.get("status") == "completed": + return True + # Native unknown outcomes need an observation already available at this + # point in the trajectory; never use a page beyond the caller's boundary. + return any(event["step_index"] < index and + (event["step_index"] > position or + (event["step_index"] == position and event["phase"] == "after")) + for event in run.events) + + +def _login_before(run, index, email): + before = _before_events(run) + typed_email, active_email = None, None + for position, step in enumerate(run.trajectory["steps"][:index]): + event = before.get(position) + if not event or not _action_ok(step): + continue + path = event["path"].rstrip("/") + if _logout_observed(run, position, index, step): + active_email = None + if path != "/login": + continue + action = normalize(step.get("action", "")).replace("_", "") + if action in {"input", "fill", "type", "inputtext", "fillform"}: + emails = [match.group().lower() for text in _strings(_params(step)) + for match in re.finditer(r"[\w.+-]+@[\w.-]+\.[a-zA-Z]{2,}", text)] + if emails: + typed_email = emails[-1] + if _interaction(step) and _control_matches(run, step, "login"): + active_email = typed_email + return active_email == email.lower() + + +def _after_page(run, index, paths, email=None): + return any(event["path"].rstrip("/") in paths + and (event["step_index"] > index or (event["step_index"] == index and event["phase"] == "after")) + and (email is None or _login_before(run, event["step_index"] + (event["phase"] == "after"), email)) + for event in run.events) + + +def _recorded_fields_agree(run, index, path, expected): + """Known form values must agree; opaque native indices are not guessed.""" + before = _before_events(run) + values = {} + for position, step in enumerate(run.trajectory["steps"][:index]): + event = before.get(position) + if not event or event["path"].rstrip("/") != path: + values.clear() + continue + if not _action_ok(step): + continue + action = normalize(step.get("action", "")).replace("_", "") + if action in {"navigate", "goto", "goback", "reload", "refresh"}: + values.clear() + if action not in {"input", "fill", "type", "inputtext", "select", "selectoption", "selectdropdownoption", "fillform"}: + continue + params = _params(step) + value = params.get("value", params.get("text", params.get("values"))) + if isinstance(value, list) and len(value) == 1: + value = value[0] + if value is None: + continue + control = _control_text(step) + identified = False + for field in expected: + if re.search(r"\b" + field + r"\b", control) or (field == "body" and "your review" in control): + values[field] = str(value).strip() + identified = True + # A native index can omit the field name while recording the actual + # new value. A unique required value corroborates that correction. + if not identified: + matched = [field for field, wanted in expected.items() if str(value).strip() == str(wanted).strip()] + if len(matched) == 1: + values[matched[0]] = str(value).strip() + return all(value == str(expected[field]).strip() for field, value in values.items()) + + +def _mutation_step(run, user, title, operation, source_paths, confirm_paths, watchlist=False, fields=None): + before = _before_events(run) + for index, step in enumerate(run.trajectory["steps"]): + event = before.get(index) + if not event or event["path"].rstrip("/") not in source_paths: + continue + if not (_action_ok(step) and _interaction(step) and _control_matches(run, step, operation, title)): + continue + if not _login_before(run, index, user["email"]): + continue + if watchlist and not any(e["path"].rstrip("/") == "/list/watchlist" and e["step_index"] <= index + and _login_before(run, e["step_index"] + (e["phase"] == "after"), user["email"]) + for e in run.events): + continue + if fields and not _recorded_fields_agree(run, index, event["path"].rstrip("/"), fields): + continue + if _after_page(run, index, confirm_paths, user["email"] if operation != "review" else None): + return index + raise VerificationError("No matching local signed-in action and subsequent confirmation page were recorded") + + +def _only_change(run, table): + changes = run.diff_tables() + _require(set(changes) == {table}, "Expected exactly one business table to change: " + table) + change = changes[table] + _require(not change["schema_changed"], "A business table's schema changed") + return change + + +def _new_system_fields(row): + valid_id = isinstance(row.get("id"), int) and row["id"] > 0 + try: + datetime.fromisoformat(row.get("created_at", "")) + except (ValueError, TypeError): + return False + return valid_id + + +def _target_text(run, title, name_optional=False, answer=None): + """Bind continuation lines while respecting other named catalog titles.""" + answer = run.answer if answer is None else answer + catalog = [dict(row) for row in run.initial.execute("SELECT id, primary_title FROM titles")] + candidates = [] + source = normalize(answer) + for row in catalog: + name = normalize(row["primary_title"]) + for match in re.finditer(r"(?= end: + named[row["id"]] = row + end = stop + if not named and name_optional: + return answer + entities = {key: [row["primary_title"]] for key, row in named.items()} + entities.setdefault(title["id"], [title["primary_title"]]) + lines, active = [], None + for line in answer.splitlines(): + matches = [key for key, names in entities.items() if any(mentions(line, name) for name in names)] + if "|" in line or not line.strip(): + active = None + elif len(matches) == 1: + active = matches[0] + elif len(matches) > 1: + active = None + elif active is not None: + line = entities[active][0] + ": " + line + lines.append(line) + return entity_texts("\n".join(lines), entities)[title["id"]] + + +def _confirmation(text, operation): + text = normalize(text) + if operation == "remove": + invalid = r"not removed|not deleted|wasn't removed|still (?:in|on)|remains? (?:in|on)" + positive = r"\b(?:removed|deleted|absent|no longer|not (?:in|on)|isn't (?:in|on)|does not appear)\b" + elif operation == "rate": + invalid = r"not (?:saved|rated|updated|confirmed)|failed|could not|couldn't" + positive = r"\b(?:rated|saved|updated|set|confirmed|verified|shows?|displayed|appears?)\b" + else: + invalid = r"not (?:visible|posted|submitted|listed|shown)|does not appear|doesn't appear|failed|could not|couldn't" + positive = r"\b(?:visible|appears?|listed|shown|confirmed|verified|present)\b" + return not re.search(invalid, text) and bool(re.search(positive, text)) + + +def _rating_answer(text, expected, required): + text = normalize(text) + text = re.sub(r"\b(?:imdb|global)\s+rating\s*(?:is|of|:)?\s*\d+(?:\.\d+)?(?:\s*/\s*10)?", "", text) + reported = re.findall(r"(?\d+(?:\.\d+)?)(?:\s*/\s*10\b)?(?!\w|\.\d|\s*/\s*\d)" + text = re.sub(r"\b(?:imdb|global)\s+rating\s*(?:is|of|:)?\s*\d+(?:\.\d+)?(?:\s*/\s*10)?", "", text) + historical = ( + r"\bfrom\s+", + r"\b(?:it\s+)?was\s+", + r"\b(?:previously|originally|initially)\s+(?:rated\s+)?", + r"\b(?:previous|prior|old|initial)\s+(?:personal\s+)?rating\s*(?:(?:was|of|is)\s+|[:=]\s*)?", + ) + for prefix in historical: + matches = [] + pattern = prefix + score + if prefix == historical[0]: + pattern += r"(?=\s+to\s+\d)" + for match in re.finditer(pattern, text): + clause = re.split(r"[.;,\n(]|\b(?:and|but)\b", text[:match.start()])[-1] + if prefix == historical[1] and re.search(r"\b(?:current|now|currently|saved|new)\b", clause): + continue + matches.append(match) + if any(previous is None or float(match["score"]) != previous for match in matches): + return False + for match in reversed(matches): + text = text[:match.start()] + " " + text[match.end():] + + # A selection condition is not a claim that the new rating has been saved. + # A negated *current* rating, however, directly contradicts the result. + eligibility = r"\b(?:other than|not)\s+" + score + for match in re.finditer(eligibility, text): + preceding = re.split(r"[.;\n]", text[:match.start()])[-1] + if (float(match["score"]) != expected or previous == expected + or re.search(r"\b(?:now|currently|saved|new)\b", preceding)): + return False + text = re.sub(eligibility, " ", text) + + # Read both /10 notation and ordinary numeric update clauses. History was + # checked against the initial snapshot above, rather than silently ignored. + current = [float(match[1]) for match in re.finditer( + r"(?is\s+|[:=]\s*)?(?P[^.;\n|]+)", text): + reported = match.group("value").strip() + # "The headline and 10/10 rating displayed" refers to an existing field. + # Explicit values such as "Headline: and beyond" must still be checked. + if not match.group("introducer") and re.match(r"(?:and|with|for|on)\b", reported): + continue + quoted = re.match(r"[\"']([^\"']+)[\"']", reported) + reported = quoted.group(1) if quoted else re.split(metadata, reported, maxsplit=1)[0] + reported = reported.strip(" \"'“”") + if reported not in {"as requested", "correct", "unchanged", "exactly as requested"}: + headlines.append(reported) + return headlines + + +def _check_17(run): + user = _user(run, "alice.j@test.com") + title = _row(run.initial, "SELECT * FROM titles WHERE primary_title=? AND year=?", ("Interstellar", 2014)) + change = _only_change(run, "reviews") + _require(not change["before"] and len(change["after"]) == 1, "Expected exactly one new review, without modifying old reviews") + review = change["after"][0] + _require(set(review) == {"id", "title_id", "user_id", "rating", "headline", "body", "helpful_count", "created_at", "is_seed"}, "The new review has unexpected fields") + _require(review["user_id"] == user["id"] and review["title_id"] == title["id"] + and review["headline"] == "Brilliant sci-fi epic" and review["rating"] == 10 + and isinstance(review["body"], str) and bool(review["body"].strip()) + and review["helpful_count"] == 0 and review["is_seed"] == 0 and _new_system_fields(review), + "The new review does not match the required author, movie, headline, rating or review fields") + _mutation_step(run, user, title, "review", {_title_path(title) + "/review"}, {_title_path(title) + "/reviews"}, + fields={"rating": 10, "headline": review["headline"], "body": review["body"]}) + text = _target_text(run, title, name_optional=True) + _require(text and _confirmation(text, "review"), "The answer does not confirm the new review appears") + _require(_rating_answer(text, 10, False), "The answer reports a different review rating") + headlines = _reported_review_headlines(text, title["primary_title"]) + _require(all(headline == "brilliant sci-fi epic" for headline in headlines), "The answer reports a different review headline") + return ["Exactly one new Alice review matches the required Interstellar headline, rating and nonempty body", + "Local review submission followed by that movie's reviews page observed; no other business table changed"] + + +def check_state_task(number, run): + """Return concise evidence strings, or raise VerificationError.""" + _require(number in STATE_TASKS, "Unsupported state task number") + _require(isinstance(run.answer, str) and bool(run.answer.strip()), "The run has no final answer") + try: + return {15: _check_15, 16: _check_16, 17: _check_17}[number](run) + except (sqlite3.Error, KeyError, TypeError, ValueError) as error: + raise VerificationError("The supplied snapshots or trajectory lack required state-task data") from error diff --git a/sites/imdb/verify/verify_0.py b/sites/imdb/verify/verify_0.py new file mode 100644 index 00000000..1825e7de --- /dev/null +++ b/sites/imdb/verify/verify_0.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_main import main + + +if __name__ == "__main__": + raise SystemExit(main(0)) diff --git a/sites/imdb/verify/verify_10.py b/sites/imdb/verify/verify_10.py new file mode 100644 index 00000000..12c2b9fa --- /dev/null +++ b/sites/imdb/verify/verify_10.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_main import main + + +if __name__ == "__main__": + raise SystemExit(main(10)) diff --git a/sites/imdb/verify/verify_12.py b/sites/imdb/verify/verify_12.py new file mode 100644 index 00000000..7462483f --- /dev/null +++ b/sites/imdb/verify/verify_12.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_main import main + + +if __name__ == "__main__": + raise SystemExit(main(12)) diff --git a/sites/imdb/verify/verify_14.py b/sites/imdb/verify/verify_14.py new file mode 100644 index 00000000..d0ba1cbf --- /dev/null +++ b/sites/imdb/verify/verify_14.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_main import main + + +if __name__ == "__main__": + raise SystemExit(main(14)) diff --git a/sites/imdb/verify/verify_15.py b/sites/imdb/verify/verify_15.py new file mode 100644 index 00000000..db4c81a2 --- /dev/null +++ b/sites/imdb/verify/verify_15.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_main import main + + +if __name__ == "__main__": + raise SystemExit(main(15)) diff --git a/sites/imdb/verify/verify_16.py b/sites/imdb/verify/verify_16.py new file mode 100644 index 00000000..17644f60 --- /dev/null +++ b/sites/imdb/verify/verify_16.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_main import main + + +if __name__ == "__main__": + raise SystemExit(main(16)) diff --git a/sites/imdb/verify/verify_17.py b/sites/imdb/verify/verify_17.py new file mode 100644 index 00000000..da45cc2e --- /dev/null +++ b/sites/imdb/verify/verify_17.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_main import main + + +if __name__ == "__main__": + raise SystemExit(main(17)) diff --git a/sites/imdb/verify/verify_18.py b/sites/imdb/verify/verify_18.py new file mode 100644 index 00000000..943d13c8 --- /dev/null +++ b/sites/imdb/verify/verify_18.py @@ -0,0 +1,5 @@ +"""Verify IMDb--18 against a supplied frozen run.""" +from verify_main import main + +if __name__ == "__main__": + raise SystemExit(main(18)) diff --git a/sites/imdb/verify/verify_19.py b/sites/imdb/verify/verify_19.py new file mode 100644 index 00000000..3973105c --- /dev/null +++ b/sites/imdb/verify/verify_19.py @@ -0,0 +1,5 @@ +"""Verify IMDb--19 against a supplied frozen run.""" +from verify_main import main + +if __name__ == "__main__": + raise SystemExit(main(19)) diff --git a/sites/imdb/verify/verify_2.py b/sites/imdb/verify/verify_2.py new file mode 100644 index 00000000..f738728c --- /dev/null +++ b/sites/imdb/verify/verify_2.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_main import main + + +if __name__ == "__main__": + raise SystemExit(main(2)) diff --git a/sites/imdb/verify/verify_20.py b/sites/imdb/verify/verify_20.py new file mode 100644 index 00000000..f5ff1618 --- /dev/null +++ b/sites/imdb/verify/verify_20.py @@ -0,0 +1,5 @@ +"""Verify IMDb--20 against a supplied frozen run.""" +from verify_main import main + +if __name__ == "__main__": + raise SystemExit(main(20)) diff --git a/sites/imdb/verify/verify_21.py b/sites/imdb/verify/verify_21.py new file mode 100644 index 00000000..52c9ba2a --- /dev/null +++ b/sites/imdb/verify/verify_21.py @@ -0,0 +1,5 @@ +"""Verify IMDb--21 against a supplied frozen run.""" +from verify_main import main + +if __name__ == "__main__": + raise SystemExit(main(21)) diff --git a/sites/imdb/verify/verify_22.py b/sites/imdb/verify/verify_22.py new file mode 100644 index 00000000..487c2f69 --- /dev/null +++ b/sites/imdb/verify/verify_22.py @@ -0,0 +1,5 @@ +"""Verify IMDb--22 against a supplied frozen run.""" +from verify_main import main + +if __name__ == "__main__": + raise SystemExit(main(22)) diff --git a/sites/imdb/verify/verify_23.py b/sites/imdb/verify/verify_23.py new file mode 100644 index 00000000..5a979e55 --- /dev/null +++ b/sites/imdb/verify/verify_23.py @@ -0,0 +1,5 @@ +"""Verify IMDb--23 against a supplied frozen run.""" +from verify_main import main + +if __name__ == "__main__": + raise SystemExit(main(23)) diff --git a/sites/imdb/verify/verify_24.py b/sites/imdb/verify/verify_24.py new file mode 100644 index 00000000..dc69e5be --- /dev/null +++ b/sites/imdb/verify/verify_24.py @@ -0,0 +1,5 @@ +"""Verify IMDb--24 against a supplied frozen run.""" +from verify_main import main + +if __name__ == "__main__": + raise SystemExit(main(24)) diff --git a/sites/imdb/verify/verify_25.py b/sites/imdb/verify/verify_25.py new file mode 100644 index 00000000..badbfd32 --- /dev/null +++ b/sites/imdb/verify/verify_25.py @@ -0,0 +1,5 @@ +"""Verify IMDb--25 against a supplied frozen run.""" +from verify_main import main + +if __name__ == "__main__": + raise SystemExit(main(25)) diff --git a/sites/imdb/verify/verify_26.py b/sites/imdb/verify/verify_26.py new file mode 100644 index 00000000..83cad5cd --- /dev/null +++ b/sites/imdb/verify/verify_26.py @@ -0,0 +1,5 @@ +"""Verify IMDb--26 against a supplied frozen run.""" +from verify_main import main + +if __name__ == "__main__": + raise SystemExit(main(26)) diff --git a/sites/imdb/verify/verify_27.py b/sites/imdb/verify/verify_27.py new file mode 100644 index 00000000..1a87cd95 --- /dev/null +++ b/sites/imdb/verify/verify_27.py @@ -0,0 +1,5 @@ +"""Verify IMDb--27 against a supplied frozen run.""" +from verify_main import main + +if __name__ == "__main__": + raise SystemExit(main(27)) diff --git a/sites/imdb/verify/verify_7.py b/sites/imdb/verify/verify_7.py new file mode 100644 index 00000000..d900cc8b --- /dev/null +++ b/sites/imdb/verify/verify_7.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_main import main + + +if __name__ == "__main__": + raise SystemExit(main(7)) diff --git a/sites/imdb/verify/verify_9.py b/sites/imdb/verify/verify_9.py new file mode 100644 index 00000000..8a4408ca --- /dev/null +++ b/sites/imdb/verify/verify_9.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_main import main + + +if __name__ == "__main__": + raise SystemExit(main(9)) diff --git a/sites/imdb/verify/verify_lib.py b/sites/imdb/verify/verify_lib.py new file mode 100644 index 00000000..124b1cae --- /dev/null +++ b/sites/imdb/verify/verify_lib.py @@ -0,0 +1,301 @@ +"""Offline run evidence shared by the IMDb deterministic verifiers. + +Only supplied run artifacts are read. URL evidence is tied to the run's local +HTTP origin; database comparisons never fall back to a live application DB. +""" + +import argparse +from collections import Counter +import json +from pathlib import Path +import re +import sqlite3 +from urllib.parse import parse_qs, urlsplit + + +class VerificationError(Exception): + """The supplied evidence is invalid or fails a verification condition.""" + + +def _local_url(url): + """Return parsed URL and normalized origin, or None for invalid input.""" + if not isinstance(url, str) or not url or any(ord(c) <= 32 or ord(c) == 127 for c in url): + return None + if "\\" in url: + return None + try: + parts = urlsplit(url) + host = parts.hostname + port = parts.port + if parts.scheme != "http" or not host or parts.username is not None or parts.password is not None: + return None + local = host in {"localhost", "127.0.0.1", "::1"} + if host.endswith(".localhost"): + prefix = host[:-len(".localhost")] + local = all(re.fullmatch(r"[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", label) + for label in prefix.split(".")) + if not local: + return None + return parts, (parts.scheme, host, port if port is not None else 80) + except (ValueError, UnicodeError): + return None + + +def _path(path): + # Permit one optional trailing slash, without normalizing case or segments. + if not isinstance(path, str) or not path.startswith("/") or "?" in path or "#" in path: + raise VerificationError("Expected an absolute pathname without query or fragment") + return path[:-1] if path != "/" and path.endswith("/") else path + + +def _successful(step): + result = step.get("action_result") + result = result if isinstance(result, dict) else {} + status = step.get("status") + if status is not None and not isinstance(status, str): + return False + if result.get("success") is not None and not isinstance(result["success"], bool): + return False + if step.get("error") or result.get("error") or result.get("success") is False: + return False + if status in {"failed", "error", "interrupted", "cancelled", "canceled", "pending", "started"}: + return False + return result.get("success") is True or status == "completed" + + +def _quote(identifier): + return '"' + identifier.replace('"', '""') + '"' + + +def _row_key(row): + # Preserve SQLite storage type distinctions (including integer versus real). + return tuple((type(value).__name__, value) for value in row) + + +def _different_rows(rows, other_rows, columns): + remaining = Counter(_row_key(row) for row in other_rows) + difference = [] + for row in rows: + key = _row_key(row) + if remaining[key]: + remaining[key] -= 1 + else: + difference.append(dict(zip(columns, row))) + return difference + + +class RunEvidence: + """Load a trajectory and two read-only SQLite snapshots. + + ``events`` contains same-origin URL observations in trajectory order, with + step_index, step, phase (before/after), url, path and parsed query fields. + Before URLs can prove a visit even when the following action failed. An + after URL requires recorded success; unknown action outcomes are excluded. + + ``diff_tables()`` returns only changed business tables. Each table provides + columns, before/after differing row dictionaries (with duplicate counts), + before_columns, after_columns and schema_changed. Raw rows may contain + sensitive fields, so callers should not print the full diff as diagnostics. + """ + + def __init__(self, run_dir, expected_task_id, expected_ques=None, + initial_db=None, after_db=None): + self.run_dir = Path(run_dir) + self.initial = None + self.after = None + try: + self.trajectory = json.loads((self.run_dir / "trajectory.json").read_text()) + except (OSError, ValueError) as error: + raise VerificationError("Cannot read a valid trajectory.json") from error + if not isinstance(self.trajectory, dict): + raise VerificationError("Trajectory must be a JSON object") + if self.trajectory.get("task_id") != expected_task_id: + raise VerificationError("Trajectory task ID does not match the expected task ID") + if expected_ques is not None: + recorded_ques = self.trajectory.get("task") + if not isinstance(recorded_ques, str) or not isinstance(expected_ques, str): + raise VerificationError("Trajectory question is missing or invalid") + if " ".join(recorded_ques.split()) != " ".join(expected_ques.split()): + raise VerificationError("Trajectory question does not match the expected question") + start = _local_url(self.trajectory.get("start_url")) + if start is None: + raise VerificationError("start_url must be a valid local HTTP URL") + self._origin = start[1] + self._steps = self.trajectory.get("steps") + if not isinstance(self._steps, list) or any(not isinstance(step, dict) for step in self._steps): + raise VerificationError("Trajectory steps must be a list of objects") + answer = self.trajectory.get("final_answer") + if answer is not None and not isinstance(answer, str): + raise VerificationError("Trajectory final_answer must be text or null") + self.answer = answer or "" + self.events = self._visit_events() + + initial_path = Path(initial_db) if initial_db is not None else self.run_dir / "initial.db" + if initial_db is None and not initial_path.exists(): + initial_path = self.run_dir / "before.db" + after_path = Path(after_db) if after_db is not None else self.run_dir / "after.db" + try: + self.initial = self._open_snapshot(initial_path, "initial") + self.after = self._open_snapshot(after_path, "after") + except BaseException: + self.close() + raise + + @staticmethod + def _open_snapshot(path, label): + if not path.is_file(): + raise VerificationError(f"Missing {label} database snapshot") + connection = None + try: + connection = sqlite3.connect(path.resolve().as_uri() + "?mode=ro", uri=True) + connection.row_factory = sqlite3.Row + connection.execute("PRAGMA query_only=ON") + connection.execute("SELECT name FROM sqlite_schema LIMIT 1").fetchall() + return connection + except sqlite3.Error as error: + if connection is not None: + connection.close() + raise VerificationError(f"Cannot read {label} database snapshot") from error + + def _visit_events(self): + events = [] + for index, step in enumerate(self._steps): + phases = [("before", step.get("url"))] + if _successful(step): + phases.append(("after", step.get("url_after"))) + for phase, url in phases: + parsed = _local_url(url) + if parsed is None or parsed[1] != self._origin: + continue + parts = parsed[0] + events.append({ + "step_index": index, + "step": step.get("step", index), + "phase": phase, + "url": url, + "path": parts.path or "/", + "query": parse_qs(parts.query, keep_blank_values=True), + }) + return events + + def successful_steps(self): + """Return original step dictionaries with an explicit successful outcome.""" + return [step for step in self._steps if _successful(step)] + + def visit_urls(self, path): + """Return matching same-origin observed URLs, preserving event order.""" + wanted = _path(path) + return [event["url"] for event in self.events if _path(event["path"]) == wanted] + + def visited(self, path, query=None): + """Match a pathname and an optional decoded query multiset subset. + + Values are compared exactly as strings. Task-specific numeric tolerance + and constraints on additional/duplicate query parameters belong to the + task verifier. + """ + wanted = _path(path) + query = {} if query is None else query + if not isinstance(query, dict) or any( + not isinstance(key, str) or not isinstance(values, list) + or any(not isinstance(value, str) for value in values) + for key, values in query.items() + ): + raise VerificationError("Expected query keys mapped to lists of strings") + for event in self.events: + if _path(event["path"]) != wanted: + continue + if all(key in event["query"] and not (Counter(values) - Counter(event["query"][key])) + for key, values in query.items()): + return True + return False + + def has_ordered_visits(self, paths): + """Check an explicitly required sequence; no home visit is implied.""" + wanted = [_path(path) for path in paths] + if not wanted: + return True + position = 0 + for event in self.events: + if _path(event["path"]) == wanted[position]: + position += 1 + if position == len(wanted): + return True + return False + + @staticmethod + def _tables(connection): + return {row[0] for row in connection.execute("SELECT name FROM sqlite_schema WHERE type='table'") + if not row[0].lower().startswith("sqlite_")} + + @staticmethod + def _table_data(connection, table, tables): + if table not in tables: + return [], [], [] + quoted = _quote(table) + cursor = connection.execute(f"SELECT * FROM {quoted}") + columns = [column[0] for column in cursor.description] + rows = [tuple(row) for row in cursor.fetchall()] + schema = [tuple(row) for row in connection.execute(f"PRAGMA table_info({quoted})")] + return columns, rows, schema + + def diff_tables(self): + """Compare business tables as typed multisets, ignoring row order.""" + try: + before_tables = self._tables(self.initial) + after_tables = self._tables(self.after) + differences = {} + for table in sorted(before_tables | after_tables): + before_columns, before, before_schema = self._table_data(self.initial, table, before_tables) + after_columns, after, after_schema = self._table_data(self.after, table, after_tables) + schema_changed = before_schema != after_schema or (table in before_tables) != (table in after_tables) + if before_columns == after_columns: + removed = _different_rows(before, after, before_columns) + added = _different_rows(after, before, after_columns) + else: + removed = [dict(zip(before_columns, row)) for row in before] + added = [dict(zip(after_columns, row)) for row in after] + if removed or added or schema_changed: + differences[table] = { + "columns": list(dict.fromkeys(before_columns + after_columns)), + "before": removed, + "after": added, + "before_columns": before_columns, + "after_columns": after_columns, + "schema_changed": schema_changed, + } + return differences + except sqlite3.Error as error: + raise VerificationError("Cannot compare database snapshots") from error + + def assert_unchanged(self, except_tables=()): + changed = sorted(set(self.diff_tables()) - set(except_tables)) + if changed: + # Do not expose row contents such as user password hashes. + raise VerificationError("Unexpected database changes in tables: " + ", ".join(changed)) + + def close(self): + for connection in (self.initial, self.after): + if connection is not None: + connection.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + +def parse_args(argv=None): + parser = argparse.ArgumentParser(description="Verify an IMDb run from offline evidence") + parser.add_argument("--run_dir", required=True) + parser.add_argument("--initial_db") + parser.add_argument("--after_db") + return parser.parse_args(argv) + + +def emit_result(task_id, passed, reason, evidence=None): + """Print one result object and return the corresponding process exit code.""" + print(json.dumps({"task_id": task_id, "pass": bool(passed), "reason": reason, + "evidence": [] if evidence is None else evidence}, ensure_ascii=False)) + return 0 if passed else 1 diff --git a/sites/imdb/verify/verify_main.py b/sites/imdb/verify/verify_main.py new file mode 100644 index 00000000..a9f306d2 --- /dev/null +++ b/sites/imdb/verify/verify_main.py @@ -0,0 +1,44 @@ +"""CLI entry point shared by the IMDb task-specific verifier scripts.""" + +import json +from pathlib import Path +import sqlite3 + +from read_tasks import READ_TASKS, check_read_task +from verify_lib import RunEvidence, VerificationError, emit_result, parse_args + + +def main(number, argv=None): + args = parse_args(argv) + task_id = f"IMDb--{number}" + try: + rows = [json.loads(line) for line in + (Path(__file__).resolve().parent.parent / "tasks.jsonl").read_text().splitlines() + if line.strip()] + matching = [row for row in rows if row.get("id") == task_id] + if len(matching) != 1: + raise VerificationError("The task must occur exactly once in the candidate task file") + with RunEvidence(args.run_dir, task_id, matching[0]["ques"], + initial_db=args.initial_db, after_db=args.after_db) as run: + if number in READ_TASKS: + evidence = check_read_task(number, run) + elif number in {15, 16, 17}: + from state_tasks import check_state_task + evidence = check_state_task(number, run) + elif number in {18, 19, 20, 23}: + from expansion_catalog_reads import check_catalog_read_task + evidence = check_catalog_read_task(number, run) + elif number in {21, 22}: + from expansion_account_reads import check_account_read_task + evidence = check_account_read_task(number, run) + elif number in {24, 25, 26, 27}: + from expansion_state_tasks import check_expansion_state_task + evidence = check_expansion_state_task(number, run) + else: + raise VerificationError("Unsupported task number") + return emit_result(task_id, True, "Task requirements satisfied by the supplied run", evidence) + except VerificationError as error: + return emit_result(task_id, False, str(error)) + except (OSError, ValueError, KeyError, sqlite3.Error) as error: + # Do not expose database rows, credentials or private filesystem paths. + return emit_result(task_id, False, f"Cannot validate the supplied artifacts ({type(error).__name__})") diff --git a/websyn_start.sh b/websyn_start.sh index 9539b4f9..0bf909be 100644 --- a/websyn_start.sh +++ b/websyn_start.sh @@ -5,7 +5,8 @@ set -e SITES=(allrecipes amazon apple arxiv bbc_news booking github google_flights google_map google_search huggingface wolfram_alpha - cambridge_dictionary coursera espn merriam_webster ikea phys_org target ted osu rotten_tomatoes compass walmart_careers) + cambridge_dictionary coursera espn merriam_webster ikea phys_org target ted osu + rotten_tomatoes compass walmart_careers imdb) BASE_PORT=40000 PID_DIR=/tmp/websyn_pids mkdir -p "$PID_DIR"