diff --git a/wizard-work/ledger/2026-10-31-albums-photo-counts.csv b/wizard-work/ledger/2026-10-31-albums-photo-counts.csv new file mode 100644 index 0000000..8d8693e --- /dev/null +++ b/wizard-work/ledger/2026-10-31-albums-photo-counts.csv @@ -0,0 +1,101 @@ +id,userId,title,photo_count +1,1,quidem molestiae enim,50 +2,1,sunt qui excepturi placeat culpa,50 +3,1,omnis laborum odio,50 +4,1,non esse culpa molestiae omnis sed optio,50 +5,1,eaque aut omnis a,50 +6,1,natus impedit quibusdam illo est,50 +7,1,quibusdam autem aliquid et et quia,50 +8,1,qui fuga est a eum,50 +9,1,saepe unde necessitatibus rem,50 +10,1,distinctio laborum qui,50 +11,2,quam nostrum impedit mollitia quod et dolor,50 +12,2,consequatur autem doloribus natus consectetur,50 +13,2,ab rerum non rerum consequatur ut ea unde,50 +14,2,ducimus molestias eos animi atque nihil,50 +15,2,ut pariatur rerum ipsum natus repellendus praesentium,50 +16,2,voluptatem aut maxime inventore autem magnam atque repellat,50 +17,2,aut minima voluptatem ut velit,50 +18,2,nesciunt quia et doloremque,50 +19,2,velit pariatur quaerat similique libero omnis quia,50 +20,2,voluptas rerum iure ut enim,50 +21,3,repudiandae voluptatem optio est consequatur rem in temporibus et,50 +22,3,et rem non provident vel ut,50 +23,3,incidunt quisquam hic adipisci sequi,50 +24,3,dolores ut et facere placeat,50 +25,3,vero maxime id possimus sunt neque et consequatur,50 +26,3,quibusdam saepe ipsa vel harum,50 +27,3,id non nostrum expedita,50 +28,3,omnis neque exercitationem sed dolor atque maxime aut cum,50 +29,3,inventore ut quasi magnam itaque est fugit,50 +30,3,tempora assumenda et similique odit distinctio error,50 +31,4,adipisci laborum fuga laboriosam,50 +32,4,reiciendis dolores a ut qui debitis non quo labore,50 +33,4,iste eos nostrum,50 +34,4,cumque voluptatibus rerum architecto blanditiis,50 +35,4,et impedit nisi quae magni necessitatibus sed aut pariatur,50 +36,4,nihil cupiditate voluptate neque,50 +37,4,est placeat dicta ut nisi rerum iste,50 +38,4,unde a sequi id,50 +39,4,ratione porro illum labore eum aperiam sed,50 +40,4,voluptas neque et sint aut quo odit,50 +41,5,ea voluptates maiores eos accusantium officiis tempore mollitia consequatur,50 +42,5,tenetur explicabo ea,50 +43,5,aperiam doloremque nihil,50 +44,5,sapiente cum numquam officia consequatur vel natus quos suscipit,50 +45,5,tenetur quos ea unde est enim corrupti qui,50 +46,5,molestiae voluptate non,50 +47,5,temporibus molestiae aut,50 +48,5,modi consequatur culpa aut quam soluta alias perspiciatis laudantium,50 +49,5,ut aut vero repudiandae voluptas ullam voluptas at consequatur,50 +50,5,sed qui sed quas sit ducimus dolor,50 +51,6,odit laboriosam sint quia cupiditate animi quis,50 +52,6,necessitatibus quas et sunt at voluptatem,50 +53,6,est vel sequi voluptatem nemo quam molestiae modi enim,50 +54,6,aut non illo amet perferendis,50 +55,6,qui culpa itaque omnis in nesciunt architecto error,50 +56,6,omnis qui maiores tempora officiis omnis rerum sed repellat,50 +57,6,libero excepturi voluptatem est architecto quae voluptatum officia tempora,50 +58,6,nulla illo consequatur aspernatur veritatis aut error delectus et,50 +59,6,eligendi similique provident nihil,50 +60,6,omnis mollitia sunt aliquid eum consequatur fugit minus laudantium,50 +61,7,delectus iusto et,50 +62,7,eos ea non recusandae iste ut quasi,50 +63,7,velit est quam,50 +64,7,autem voluptatem amet iure quae,50 +65,7,voluptates delectus iure iste qui,50 +66,7,velit sed quia dolor dolores delectus,50 +67,7,ad voluptas nostrum et nihil,50 +68,7,qui quasi nihil aut voluptatum sit dolore minima,50 +69,7,qui aut est,50 +70,7,et deleniti unde,50 +71,8,et vel corporis,50 +72,8,unde exercitationem ut,50 +73,8,quos omnis officia,50 +74,8,quia est eius vitae dolor,50 +75,8,aut quia expedita non,50 +76,8,dolorem magnam facere itaque ut reprehenderit tenetur corrupti,50 +77,8,cupiditate sapiente maiores iusto ducimus cum excepturi veritatis quia,50 +78,8,est minima eius possimus ea ratione velit et,50 +79,8,ipsa quae voluptas natus ut suscipit soluta quia quidem,50 +80,8,id nihil reprehenderit,50 +81,9,quibusdam sapiente et,50 +82,9,recusandae consequatur vel amet unde,50 +83,9,aperiam odio fugiat,50 +84,9,est et at eos expedita,50 +85,9,qui voluptatem consequatur aut ab quis temporibus praesentium,50 +86,9,eligendi mollitia alias aspernatur vel ut iusto,50 +87,9,aut aut architecto,50 +88,9,quas perspiciatis optio,50 +89,9,sit optio id voluptatem est eum et,50 +90,9,est vel dignissimos,50 +91,10,repellendus praesentium debitis officiis,50 +92,10,incidunt et et eligendi assumenda soluta quia recusandae,50 +93,10,nisi qui dolores perspiciatis,50 +94,10,quisquam a dolores et earum vitae,50 +95,10,consectetur vel rerum qui aperiam modi eos aspernatur ipsa,50 +96,10,unde et ut molestiae est molestias voluptatem sint,50 +97,10,est quod aut,50 +98,10,omnis quia possimus nesciunt deleniti assumenda sed autem,50 +99,10,consectetur ut id impedit dolores sit ad ex aut,50 +100,10,enim repellat iste,50 diff --git a/wizard-work/ledger/2026-10-31-albums-photo-counts.json b/wizard-work/ledger/2026-10-31-albums-photo-counts.json new file mode 100644 index 0000000..f40fcb4 --- /dev/null +++ b/wizard-work/ledger/2026-10-31-albums-photo-counts.json @@ -0,0 +1,602 @@ +[ + { + "id": 1, + "userId": 1, + "title": "quidem molestiae enim", + "photo_count": 50 + }, + { + "id": 2, + "userId": 1, + "title": "sunt qui excepturi placeat culpa", + "photo_count": 50 + }, + { + "id": 3, + "userId": 1, + "title": "omnis laborum odio", + "photo_count": 50 + }, + { + "id": 4, + "userId": 1, + "title": "non esse culpa molestiae omnis sed optio", + "photo_count": 50 + }, + { + "id": 5, + "userId": 1, + "title": "eaque aut omnis a", + "photo_count": 50 + }, + { + "id": 6, + "userId": 1, + "title": "natus impedit quibusdam illo est", + "photo_count": 50 + }, + { + "id": 7, + "userId": 1, + "title": "quibusdam autem aliquid et et quia", + "photo_count": 50 + }, + { + "id": 8, + "userId": 1, + "title": "qui fuga est a eum", + "photo_count": 50 + }, + { + "id": 9, + "userId": 1, + "title": "saepe unde necessitatibus rem", + "photo_count": 50 + }, + { + "id": 10, + "userId": 1, + "title": "distinctio laborum qui", + "photo_count": 50 + }, + { + "id": 11, + "userId": 2, + "title": "quam nostrum impedit mollitia quod et dolor", + "photo_count": 50 + }, + { + "id": 12, + "userId": 2, + "title": "consequatur autem doloribus natus consectetur", + "photo_count": 50 + }, + { + "id": 13, + "userId": 2, + "title": "ab rerum non rerum consequatur ut ea unde", + "photo_count": 50 + }, + { + "id": 14, + "userId": 2, + "title": "ducimus molestias eos animi atque nihil", + "photo_count": 50 + }, + { + "id": 15, + "userId": 2, + "title": "ut pariatur rerum ipsum natus repellendus praesentium", + "photo_count": 50 + }, + { + "id": 16, + "userId": 2, + "title": "voluptatem aut maxime inventore autem magnam atque repellat", + "photo_count": 50 + }, + { + "id": 17, + "userId": 2, + "title": "aut minima voluptatem ut velit", + "photo_count": 50 + }, + { + "id": 18, + "userId": 2, + "title": "nesciunt quia et doloremque", + "photo_count": 50 + }, + { + "id": 19, + "userId": 2, + "title": "velit pariatur quaerat similique libero omnis quia", + "photo_count": 50 + }, + { + "id": 20, + "userId": 2, + "title": "voluptas rerum iure ut enim", + "photo_count": 50 + }, + { + "id": 21, + "userId": 3, + "title": "repudiandae voluptatem optio est consequatur rem in temporibus et", + "photo_count": 50 + }, + { + "id": 22, + "userId": 3, + "title": "et rem non provident vel ut", + "photo_count": 50 + }, + { + "id": 23, + "userId": 3, + "title": "incidunt quisquam hic adipisci sequi", + "photo_count": 50 + }, + { + "id": 24, + "userId": 3, + "title": "dolores ut et facere placeat", + "photo_count": 50 + }, + { + "id": 25, + "userId": 3, + "title": "vero maxime id possimus sunt neque et consequatur", + "photo_count": 50 + }, + { + "id": 26, + "userId": 3, + "title": "quibusdam saepe ipsa vel harum", + "photo_count": 50 + }, + { + "id": 27, + "userId": 3, + "title": "id non nostrum expedita", + "photo_count": 50 + }, + { + "id": 28, + "userId": 3, + "title": "omnis neque exercitationem sed dolor atque maxime aut cum", + "photo_count": 50 + }, + { + "id": 29, + "userId": 3, + "title": "inventore ut quasi magnam itaque est fugit", + "photo_count": 50 + }, + { + "id": 30, + "userId": 3, + "title": "tempora assumenda et similique odit distinctio error", + "photo_count": 50 + }, + { + "id": 31, + "userId": 4, + "title": "adipisci laborum fuga laboriosam", + "photo_count": 50 + }, + { + "id": 32, + "userId": 4, + "title": "reiciendis dolores a ut qui debitis non quo labore", + "photo_count": 50 + }, + { + "id": 33, + "userId": 4, + "title": "iste eos nostrum", + "photo_count": 50 + }, + { + "id": 34, + "userId": 4, + "title": "cumque voluptatibus rerum architecto blanditiis", + "photo_count": 50 + }, + { + "id": 35, + "userId": 4, + "title": "et impedit nisi quae magni necessitatibus sed aut pariatur", + "photo_count": 50 + }, + { + "id": 36, + "userId": 4, + "title": "nihil cupiditate voluptate neque", + "photo_count": 50 + }, + { + "id": 37, + "userId": 4, + "title": "est placeat dicta ut nisi rerum iste", + "photo_count": 50 + }, + { + "id": 38, + "userId": 4, + "title": "unde a sequi id", + "photo_count": 50 + }, + { + "id": 39, + "userId": 4, + "title": "ratione porro illum labore eum aperiam sed", + "photo_count": 50 + }, + { + "id": 40, + "userId": 4, + "title": "voluptas neque et sint aut quo odit", + "photo_count": 50 + }, + { + "id": 41, + "userId": 5, + "title": "ea voluptates maiores eos accusantium officiis tempore mollitia consequatur", + "photo_count": 50 + }, + { + "id": 42, + "userId": 5, + "title": "tenetur explicabo ea", + "photo_count": 50 + }, + { + "id": 43, + "userId": 5, + "title": "aperiam doloremque nihil", + "photo_count": 50 + }, + { + "id": 44, + "userId": 5, + "title": "sapiente cum numquam officia consequatur vel natus quos suscipit", + "photo_count": 50 + }, + { + "id": 45, + "userId": 5, + "title": "tenetur quos ea unde est enim corrupti qui", + "photo_count": 50 + }, + { + "id": 46, + "userId": 5, + "title": "molestiae voluptate non", + "photo_count": 50 + }, + { + "id": 47, + "userId": 5, + "title": "temporibus molestiae aut", + "photo_count": 50 + }, + { + "id": 48, + "userId": 5, + "title": "modi consequatur culpa aut quam soluta alias perspiciatis laudantium", + "photo_count": 50 + }, + { + "id": 49, + "userId": 5, + "title": "ut aut vero repudiandae voluptas ullam voluptas at consequatur", + "photo_count": 50 + }, + { + "id": 50, + "userId": 5, + "title": "sed qui sed quas sit ducimus dolor", + "photo_count": 50 + }, + { + "id": 51, + "userId": 6, + "title": "odit laboriosam sint quia cupiditate animi quis", + "photo_count": 50 + }, + { + "id": 52, + "userId": 6, + "title": "necessitatibus quas et sunt at voluptatem", + "photo_count": 50 + }, + { + "id": 53, + "userId": 6, + "title": "est vel sequi voluptatem nemo quam molestiae modi enim", + "photo_count": 50 + }, + { + "id": 54, + "userId": 6, + "title": "aut non illo amet perferendis", + "photo_count": 50 + }, + { + "id": 55, + "userId": 6, + "title": "qui culpa itaque omnis in nesciunt architecto error", + "photo_count": 50 + }, + { + "id": 56, + "userId": 6, + "title": "omnis qui maiores tempora officiis omnis rerum sed repellat", + "photo_count": 50 + }, + { + "id": 57, + "userId": 6, + "title": "libero excepturi voluptatem est architecto quae voluptatum officia tempora", + "photo_count": 50 + }, + { + "id": 58, + "userId": 6, + "title": "nulla illo consequatur aspernatur veritatis aut error delectus et", + "photo_count": 50 + }, + { + "id": 59, + "userId": 6, + "title": "eligendi similique provident nihil", + "photo_count": 50 + }, + { + "id": 60, + "userId": 6, + "title": "omnis mollitia sunt aliquid eum consequatur fugit minus laudantium", + "photo_count": 50 + }, + { + "id": 61, + "userId": 7, + "title": "delectus iusto et", + "photo_count": 50 + }, + { + "id": 62, + "userId": 7, + "title": "eos ea non recusandae iste ut quasi", + "photo_count": 50 + }, + { + "id": 63, + "userId": 7, + "title": "velit est quam", + "photo_count": 50 + }, + { + "id": 64, + "userId": 7, + "title": "autem voluptatem amet iure quae", + "photo_count": 50 + }, + { + "id": 65, + "userId": 7, + "title": "voluptates delectus iure iste qui", + "photo_count": 50 + }, + { + "id": 66, + "userId": 7, + "title": "velit sed quia dolor dolores delectus", + "photo_count": 50 + }, + { + "id": 67, + "userId": 7, + "title": "ad voluptas nostrum et nihil", + "photo_count": 50 + }, + { + "id": 68, + "userId": 7, + "title": "qui quasi nihil aut voluptatum sit dolore minima", + "photo_count": 50 + }, + { + "id": 69, + "userId": 7, + "title": "qui aut est", + "photo_count": 50 + }, + { + "id": 70, + "userId": 7, + "title": "et deleniti unde", + "photo_count": 50 + }, + { + "id": 71, + "userId": 8, + "title": "et vel corporis", + "photo_count": 50 + }, + { + "id": 72, + "userId": 8, + "title": "unde exercitationem ut", + "photo_count": 50 + }, + { + "id": 73, + "userId": 8, + "title": "quos omnis officia", + "photo_count": 50 + }, + { + "id": 74, + "userId": 8, + "title": "quia est eius vitae dolor", + "photo_count": 50 + }, + { + "id": 75, + "userId": 8, + "title": "aut quia expedita non", + "photo_count": 50 + }, + { + "id": 76, + "userId": 8, + "title": "dolorem magnam facere itaque ut reprehenderit tenetur corrupti", + "photo_count": 50 + }, + { + "id": 77, + "userId": 8, + "title": "cupiditate sapiente maiores iusto ducimus cum excepturi veritatis quia", + "photo_count": 50 + }, + { + "id": 78, + "userId": 8, + "title": "est minima eius possimus ea ratione velit et", + "photo_count": 50 + }, + { + "id": 79, + "userId": 8, + "title": "ipsa quae voluptas natus ut suscipit soluta quia quidem", + "photo_count": 50 + }, + { + "id": 80, + "userId": 8, + "title": "id nihil reprehenderit", + "photo_count": 50 + }, + { + "id": 81, + "userId": 9, + "title": "quibusdam sapiente et", + "photo_count": 50 + }, + { + "id": 82, + "userId": 9, + "title": "recusandae consequatur vel amet unde", + "photo_count": 50 + }, + { + "id": 83, + "userId": 9, + "title": "aperiam odio fugiat", + "photo_count": 50 + }, + { + "id": 84, + "userId": 9, + "title": "est et at eos expedita", + "photo_count": 50 + }, + { + "id": 85, + "userId": 9, + "title": "qui voluptatem consequatur aut ab quis temporibus praesentium", + "photo_count": 50 + }, + { + "id": 86, + "userId": 9, + "title": "eligendi mollitia alias aspernatur vel ut iusto", + "photo_count": 50 + }, + { + "id": 87, + "userId": 9, + "title": "aut aut architecto", + "photo_count": 50 + }, + { + "id": 88, + "userId": 9, + "title": "quas perspiciatis optio", + "photo_count": 50 + }, + { + "id": 89, + "userId": 9, + "title": "sit optio id voluptatem est eum et", + "photo_count": 50 + }, + { + "id": 90, + "userId": 9, + "title": "est vel dignissimos", + "photo_count": 50 + }, + { + "id": 91, + "userId": 10, + "title": "repellendus praesentium debitis officiis", + "photo_count": 50 + }, + { + "id": 92, + "userId": 10, + "title": "incidunt et et eligendi assumenda soluta quia recusandae", + "photo_count": 50 + }, + { + "id": 93, + "userId": 10, + "title": "nisi qui dolores perspiciatis", + "photo_count": 50 + }, + { + "id": 94, + "userId": 10, + "title": "quisquam a dolores et earum vitae", + "photo_count": 50 + }, + { + "id": 95, + "userId": 10, + "title": "consectetur vel rerum qui aperiam modi eos aspernatur ipsa", + "photo_count": 50 + }, + { + "id": 96, + "userId": 10, + "title": "unde et ut molestiae est molestias voluptatem sint", + "photo_count": 50 + }, + { + "id": 97, + "userId": 10, + "title": "est quod aut", + "photo_count": 50 + }, + { + "id": 98, + "userId": 10, + "title": "omnis quia possimus nesciunt deleniti assumenda sed autem", + "photo_count": 50 + }, + { + "id": 99, + "userId": 10, + "title": "consectetur ut id impedit dolores sit ad ex aut", + "photo_count": 50 + }, + { + "id": 100, + "userId": 10, + "title": "enim repellat iste", + "photo_count": 50 + } +] \ No newline at end of file diff --git a/wizard-work/ledger/2026-10-31-albums-photo-counts.md b/wizard-work/ledger/2026-10-31-albums-photo-counts.md new file mode 100644 index 0000000..4cae279 --- /dev/null +++ b/wizard-work/ledger/2026-10-31-albums-photo-counts.md @@ -0,0 +1,43 @@ +# Albums → Photos join (extract artifact) — ledger lane + +Per-album table: which album, its author (`userId`), its title, and the number of +photos it contains. One row per album. + +## Sources + +- `https://jsonplaceholder.typicode.com/albums` — 100 albums +- `https://jsonplaceholder.typicode.com/photos` — 5000 photos + +Join key: `photos.albumId == albums.id`. + +## Columns + +| Column | Meaning | +|---|---| +| `id` | album id (1–100) | +| `userId` | album author | +| `title` | album title | +| `photo_count` | number of photos in the album | + +## Result (100 rows) + +- 100 albums, 5000 photos, 100 distinct album ids. +- `photo_count` is **50 for every album** (uniform). + +## Artifacts (reproduced by `extract_albums_photo_counts.py`) + +- `2026-10-31-albums-photo-counts.csv` — the table (CSV) +- `2026-10-31-albums-photo-counts.json` — the same table (JSON) + +## Verification (single command, exit 0 = correct) + +Run from the repo root: + +```bash +bash wizard-work/ledger/CHECK-albums-photo-counts +``` + +The check re-fetches both endpoints, re-derives the table, and compares the +committed CSV and JSON against the fresh extraction (parsed, so line endings and +quoting do not affect the verdict). It also asserts the row counts, the total +photo count and the uniform `photo_count` claim above. diff --git a/wizard-work/ledger/CHECK-albums-photo-counts b/wizard-work/ledger/CHECK-albums-photo-counts new file mode 100755 index 0000000..c66d2bc --- /dev/null +++ b/wizard-work/ledger/CHECK-albums-photo-counts @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# CHECK -- albums->photos extract (Extract lane, 2026-10-31). +# Re-fetches both JSONPlaceholder endpoints, re-derives the per-album table, and +# compares the committed CSV + JSON against the fresh extraction. Exits 0 only +# when the committed artifacts match. Exit code IS the verdict. +set -u + +DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +cd "$DIR/../.." || exit 1 + +python3 wizard-work/ledger/verify_albums_photo_counts.py diff --git a/wizard-work/ledger/extract_albums_photo_counts.py b/wizard-work/ledger/extract_albums_photo_counts.py new file mode 100644 index 0000000..4d8fd51 --- /dev/null +++ b/wizard-work/ledger/extract_albums_photo_counts.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Extract: JSONPlaceholder albums -> photos join. + +Builds a per-album table: which album, its author (userId), its title, and the +number of photos it contains. Sources: + + https://jsonplaceholder.typicode.com/albums (100 albums) + https://jsonplaceholder.typicode.com/photos (5000 photos) + +Join key: photos.albumId == albums.id + +One row per album (100 rows). photo_count is 50 for every album (uniform). +Outputs: CSV + JSON of the same table. +""" + +import csv +import json +import os +import urllib.request +from collections import Counter, defaultdict + +ALBUMS_URL = "https://jsonplaceholder.typicode.com/albums" +PHOTOS_URL = "https://jsonplaceholder.typicode.com/photos" + +BASE = os.path.dirname(os.path.abspath(__file__)) + + +def fetch(url): + req = urllib.request.Request(url, headers={"User-Agent": "ledger/extract"}) + with urllib.request.urlopen(req, timeout=30) as r: + return json.load(r) + + +def main(): + albums = fetch(ALBUMS_URL) + photos = fetch(PHOTOS_URL) + + # sanity: every photo must resolve to a known album + album_ids = {a["id"] for a in albums} + unknown = sorted({p["albumId"] for p in photos} - album_ids) + if unknown: + raise SystemExit("photo albumIds not in albums: %s" % unknown[:5]) + + counts = Counter(p["albumId"] for p in photos) + total = sum(counts.values()) + + rows = [] + for a in sorted(albums, key=lambda x: x["id"]): + rows.append({ + "id": a["id"], + "userId": a["userId"], + "title": a["title"], + "photo_count": counts.get(a["id"], 0), + }) + + csv_path = os.path.join(BASE, "2026-10-31-albums-photo-counts.csv") + json_path = os.path.join(BASE, "2026-10-31-albums-photo-counts.json") + + with open(csv_path, "w", newline="", encoding="utf-8") as f: + w = csv.DictWriter(f, fieldnames=["id", "userId", "title", "photo_count"]) + w.writeheader() + w.writerows(rows) + + payload = json.dumps(rows, ensure_ascii=False, indent=2).encode("utf-8") + with open(json_path, "wb") as f: + f.write(payload) + + with open(csv_path, encoding="utf-8") as f: + csv_bytes = len(f.read()) + print("wrote %s (%d bytes, %d rows)" % (csv_path, csv_bytes, len(rows))) + print("wrote %s (%d bytes)" % (json_path, len(payload))) + print("total photos = %d across %d albums (uniform photo_count=%d)" + % (total, len(rows), rows[0]["photo_count"] if rows else None)) + + +if __name__ == "__main__": + main() diff --git a/wizard-work/ledger/verify_albums_photo_counts.py b/wizard-work/ledger/verify_albums_photo_counts.py new file mode 100644 index 0000000..ee66475 --- /dev/null +++ b/wizard-work/ledger/verify_albums_photo_counts.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Independent verifier for the albums->photos extract artifact (ledger lane). + +Re-fetches both JSONPlaceholder endpoints, re-derives the per-album table, and +compares the committed CSV + JSON against the fresh extraction (parsed, so line +endings and quoting do not affect the verdict). Also asserts the row counts, +the total photo count and the uniform photo_count claim above. Exits 0 only when +the committed artifacts match the fresh extraction. +""" + +import csv +import json +import sys +import urllib.request +from collections import Counter + +BASE = "/root/troupe-standup/wizard-work/ledger" +CSV_PATH = BASE + "/2026-10-31-albums-photo-counts.csv" +JSON_PATH = BASE + "/2026-10-31-albums-photo-counts.json" + +ALBUMS_URL = "https://jsonplaceholder.typicode.com/albums" +PHOTOS_URL = "https://jsonplaceholder.typicode.com/photos" + + +def fetch(url): + req = urllib.request.Request(url, headers={"User-Agent": "ledger/extract"}) + with urllib.request.urlopen(req, timeout=30) as r: + return json.load(r) + + +def read_csv_rows(path): + with open(path, encoding="utf-8") as f: + return list(csv.DictReader(f)) + + +def main(): + albums = fetch(ALBUMS_URL) + photos = fetch(PHOTOS_URL) + + album_ids = {a["id"] for a in albums} + unknown = sorted({p["albumId"] for p in photos} - album_ids) + if unknown: + print("FAIL: photo albumIds not present in albums: %s" % unknown[:5]) + return 1 + + counts = Counter(p["albumId"] for p in photos) + total = sum(counts.values()) + + # Fresh extraction result (same field types the extractor writes). + raw_rows = [] + for a in sorted(albums, key=lambda x: x["id"]): + raw_rows.append({ + "id": a["id"], + "userId": a["userId"], + "title": a["title"], + "photo_count": counts.get(a["id"], 0), + }) + + if len(albums) != 100: + print("FAIL: expected 100 albums, got %d" % len(albums)) + return 1 + if len(photos) != 5000: + print("FAIL: expected 5000 photos, got %d" % len(photos)) + return 1 + if len(counts) != 100: + print("FAIL: expected 100 distinct album ids with photos, got %d" % len(counts)) + return 1 + if len(set(counts.values())) != 1: + print("FAIL: photo_count not uniform, values=%s" % sorted(set(counts.values()))) + return 1 + + # CSV: committed file parsed with DictReader -> all strings. + csv_rows = [] + for r in raw_rows: + csv_rows.append({ + "id": str(r["id"]), + "userId": str(r["userId"]), + "title": r["title"], + "photo_count": str(r["photo_count"]), + }) + if read_csv_rows(CSV_PATH) != csv_rows: + print("FAIL: CSV does not match fresh extraction") + return 1 + + # JSON: committed file parsed -> ints for id/userId/photo_count. + if json.load(open(JSON_PATH, encoding="utf-8")) != raw_rows: + print("FAIL: JSON does not match fresh extraction") + return 1 + + print("OK: %d rows match the fresh extraction (uniform photo_count=50, %d photos)" + % (len(raw_rows), total)) + return 0 + + +if __name__ == "__main__": + sys.exit(main())