diff --git a/.gitignore b/.gitignore index 6d2ba39..df4e135 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,7 @@ assets/references/ # Gods of AI (Series Four) working dirs. # node scripts/gods-cards.mjs all dist/gods/ + +# Women in Tech (Series Five) working dirs. +# node scripts/women-cards.mjs all +dist/women/ diff --git a/README.md b/README.md index 06accf5..30a603d 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ signups into the `waitlist` table. ## Card production -Four decks, one per series. Approved portrait art lives under +Five decks, one per series. Approved portrait art lives under `assets/portraits//` — see [assets/portraits/README.md](assets/portraits/README.md). **Series One — Open Source Legends** (`data/roster.locked.json`, complete): @@ -152,6 +152,33 @@ Bulgarian basketball player for "Georgi Gerganov". And a reference whose subject head is cropped out of frame, or whose avatar is a felt sculpture, makes the model invent a face — which is why François Chollet has no portrait. +**Series Five — Women in Tech** (`src/data/women.ts`, 50 cards): + +```bash +pnpm women:refs # find + verify a reference photo per card +pnpm women all # portraits -> render -> publish +``` + +180 years, from Lovelace to the people auditing models now. Rated on `technical`, +`pioneering`, `openness`, `influence`. 43 of 50 have a verified photograph. + +**The date guard had to come out for this set.** Series Three rejects any reference +photograph dated before 1950, because an old plate meant the search had found a +historical namesake. Here the historical figures ARE the subject, so that guard +would have silently deleted Lovelace, Hopper and every ENIAC and Bletchley entry. +`women-refs.mjs` has no date or birth-year floor. Read the comment before copying +the resolver to a sixth set. + +What the review caught this time: a photograph of the **house** carrying Joan +Clarke's blue plaque, a **screenshot of a Wikidata item** for Anita Borg, a +**different Julia Evans**, a **French astronaut** named Camille Fournier, and two +ENIAC room shots where no face is legible. Léonie Watson's avatar is her logo and +Julia Evans' is a cartoon star, which is the Chollet failure again: give the model +a reference with no face and it will invent one rather than fail. + +Nobody appears in more than one series, so the fifteen women already carded in +Series Two to Four are linked from the roster page instead of repeated. + ## Project layout ``` diff --git a/assets/portraits/women-in-tech/card_001.png b/assets/portraits/women-in-tech/card_001.png new file mode 100644 index 0000000..c5fc52f Binary files /dev/null and b/assets/portraits/women-in-tech/card_001.png differ diff --git a/assets/portraits/women-in-tech/card_002.png b/assets/portraits/women-in-tech/card_002.png new file mode 100644 index 0000000..6a1f3b2 Binary files /dev/null and b/assets/portraits/women-in-tech/card_002.png differ diff --git a/assets/portraits/women-in-tech/card_003.png b/assets/portraits/women-in-tech/card_003.png new file mode 100644 index 0000000..65206f1 Binary files /dev/null and b/assets/portraits/women-in-tech/card_003.png differ diff --git a/assets/portraits/women-in-tech/card_004.png b/assets/portraits/women-in-tech/card_004.png new file mode 100644 index 0000000..b3e2d2d Binary files /dev/null and b/assets/portraits/women-in-tech/card_004.png differ diff --git a/assets/portraits/women-in-tech/card_005.png b/assets/portraits/women-in-tech/card_005.png new file mode 100644 index 0000000..f6ab17f Binary files /dev/null and b/assets/portraits/women-in-tech/card_005.png differ diff --git a/assets/portraits/women-in-tech/card_009.png b/assets/portraits/women-in-tech/card_009.png new file mode 100644 index 0000000..5ef87c6 Binary files /dev/null and b/assets/portraits/women-in-tech/card_009.png differ diff --git a/assets/portraits/women-in-tech/card_010.png b/assets/portraits/women-in-tech/card_010.png new file mode 100644 index 0000000..6c72851 Binary files /dev/null and b/assets/portraits/women-in-tech/card_010.png differ diff --git a/assets/portraits/women-in-tech/card_011.png b/assets/portraits/women-in-tech/card_011.png new file mode 100644 index 0000000..49412e9 Binary files /dev/null and b/assets/portraits/women-in-tech/card_011.png differ diff --git a/assets/portraits/women-in-tech/card_012.png b/assets/portraits/women-in-tech/card_012.png new file mode 100644 index 0000000..6ab4cc5 Binary files /dev/null and b/assets/portraits/women-in-tech/card_012.png differ diff --git a/assets/portraits/women-in-tech/card_014.png b/assets/portraits/women-in-tech/card_014.png new file mode 100644 index 0000000..27b1db6 Binary files /dev/null and b/assets/portraits/women-in-tech/card_014.png differ diff --git a/assets/portraits/women-in-tech/card_015.png b/assets/portraits/women-in-tech/card_015.png new file mode 100644 index 0000000..2a50130 Binary files /dev/null and b/assets/portraits/women-in-tech/card_015.png differ diff --git a/assets/portraits/women-in-tech/card_016.png b/assets/portraits/women-in-tech/card_016.png new file mode 100644 index 0000000..eab9893 Binary files /dev/null and b/assets/portraits/women-in-tech/card_016.png differ diff --git a/assets/portraits/women-in-tech/card_017.png b/assets/portraits/women-in-tech/card_017.png new file mode 100644 index 0000000..8235ba8 Binary files /dev/null and b/assets/portraits/women-in-tech/card_017.png differ diff --git a/assets/portraits/women-in-tech/card_018.png b/assets/portraits/women-in-tech/card_018.png new file mode 100644 index 0000000..2701518 Binary files /dev/null and b/assets/portraits/women-in-tech/card_018.png differ diff --git a/assets/portraits/women-in-tech/card_019.png b/assets/portraits/women-in-tech/card_019.png new file mode 100644 index 0000000..514a2b5 Binary files /dev/null and b/assets/portraits/women-in-tech/card_019.png differ diff --git a/assets/portraits/women-in-tech/card_020.png b/assets/portraits/women-in-tech/card_020.png new file mode 100644 index 0000000..82343c5 Binary files /dev/null and b/assets/portraits/women-in-tech/card_020.png differ diff --git a/assets/portraits/women-in-tech/card_022.png b/assets/portraits/women-in-tech/card_022.png new file mode 100644 index 0000000..5055cba Binary files /dev/null and b/assets/portraits/women-in-tech/card_022.png differ diff --git a/assets/portraits/women-in-tech/card_023.png b/assets/portraits/women-in-tech/card_023.png new file mode 100644 index 0000000..fba9eb6 Binary files /dev/null and b/assets/portraits/women-in-tech/card_023.png differ diff --git a/assets/portraits/women-in-tech/card_024.png b/assets/portraits/women-in-tech/card_024.png new file mode 100644 index 0000000..a5e4b7e Binary files /dev/null and b/assets/portraits/women-in-tech/card_024.png differ diff --git a/assets/portraits/women-in-tech/card_025.png b/assets/portraits/women-in-tech/card_025.png new file mode 100644 index 0000000..9d7c547 Binary files /dev/null and b/assets/portraits/women-in-tech/card_025.png differ diff --git a/assets/portraits/women-in-tech/card_026.png b/assets/portraits/women-in-tech/card_026.png new file mode 100644 index 0000000..7e5ab41 Binary files /dev/null and b/assets/portraits/women-in-tech/card_026.png differ diff --git a/assets/portraits/women-in-tech/card_027.png b/assets/portraits/women-in-tech/card_027.png new file mode 100644 index 0000000..b4bcdc5 Binary files /dev/null and b/assets/portraits/women-in-tech/card_027.png differ diff --git a/assets/portraits/women-in-tech/card_028.png b/assets/portraits/women-in-tech/card_028.png new file mode 100644 index 0000000..d99922b Binary files /dev/null and b/assets/portraits/women-in-tech/card_028.png differ diff --git a/assets/portraits/women-in-tech/card_029.png b/assets/portraits/women-in-tech/card_029.png new file mode 100644 index 0000000..6c72ca7 Binary files /dev/null and b/assets/portraits/women-in-tech/card_029.png differ diff --git a/assets/portraits/women-in-tech/card_030.png b/assets/portraits/women-in-tech/card_030.png new file mode 100644 index 0000000..d0659d9 Binary files /dev/null and b/assets/portraits/women-in-tech/card_030.png differ diff --git a/assets/portraits/women-in-tech/card_031.png b/assets/portraits/women-in-tech/card_031.png new file mode 100644 index 0000000..8c7858f Binary files /dev/null and b/assets/portraits/women-in-tech/card_031.png differ diff --git a/assets/portraits/women-in-tech/card_032.png b/assets/portraits/women-in-tech/card_032.png new file mode 100644 index 0000000..41b1b9f Binary files /dev/null and b/assets/portraits/women-in-tech/card_032.png differ diff --git a/assets/portraits/women-in-tech/card_033.png b/assets/portraits/women-in-tech/card_033.png new file mode 100644 index 0000000..0e5bfc0 Binary files /dev/null and b/assets/portraits/women-in-tech/card_033.png differ diff --git a/assets/portraits/women-in-tech/card_034.png b/assets/portraits/women-in-tech/card_034.png new file mode 100644 index 0000000..27553ea Binary files /dev/null and b/assets/portraits/women-in-tech/card_034.png differ diff --git a/assets/portraits/women-in-tech/card_035.png b/assets/portraits/women-in-tech/card_035.png new file mode 100644 index 0000000..29e646a Binary files /dev/null and b/assets/portraits/women-in-tech/card_035.png differ diff --git a/assets/portraits/women-in-tech/card_036.png b/assets/portraits/women-in-tech/card_036.png new file mode 100644 index 0000000..cc72ac8 Binary files /dev/null and b/assets/portraits/women-in-tech/card_036.png differ diff --git a/assets/portraits/women-in-tech/card_037.png b/assets/portraits/women-in-tech/card_037.png new file mode 100644 index 0000000..726b695 Binary files /dev/null and b/assets/portraits/women-in-tech/card_037.png differ diff --git a/assets/portraits/women-in-tech/card_039.png b/assets/portraits/women-in-tech/card_039.png new file mode 100644 index 0000000..6d12dcd Binary files /dev/null and b/assets/portraits/women-in-tech/card_039.png differ diff --git a/assets/portraits/women-in-tech/card_040.png b/assets/portraits/women-in-tech/card_040.png new file mode 100644 index 0000000..01e5324 Binary files /dev/null and b/assets/portraits/women-in-tech/card_040.png differ diff --git a/assets/portraits/women-in-tech/card_041.png b/assets/portraits/women-in-tech/card_041.png new file mode 100644 index 0000000..373fc8e Binary files /dev/null and b/assets/portraits/women-in-tech/card_041.png differ diff --git a/assets/portraits/women-in-tech/card_042.png b/assets/portraits/women-in-tech/card_042.png new file mode 100644 index 0000000..b5238d5 Binary files /dev/null and b/assets/portraits/women-in-tech/card_042.png differ diff --git a/assets/portraits/women-in-tech/card_044.png b/assets/portraits/women-in-tech/card_044.png new file mode 100644 index 0000000..1ef90ea Binary files /dev/null and b/assets/portraits/women-in-tech/card_044.png differ diff --git a/assets/portraits/women-in-tech/card_045.png b/assets/portraits/women-in-tech/card_045.png new file mode 100644 index 0000000..163416b Binary files /dev/null and b/assets/portraits/women-in-tech/card_045.png differ diff --git a/assets/portraits/women-in-tech/card_046.png b/assets/portraits/women-in-tech/card_046.png new file mode 100644 index 0000000..0e5ad83 Binary files /dev/null and b/assets/portraits/women-in-tech/card_046.png differ diff --git a/assets/portraits/women-in-tech/card_047.png b/assets/portraits/women-in-tech/card_047.png new file mode 100644 index 0000000..0dbba1c Binary files /dev/null and b/assets/portraits/women-in-tech/card_047.png differ diff --git a/assets/portraits/women-in-tech/card_048.png b/assets/portraits/women-in-tech/card_048.png new file mode 100644 index 0000000..089082d Binary files /dev/null and b/assets/portraits/women-in-tech/card_048.png differ diff --git a/assets/portraits/women-in-tech/card_049.png b/assets/portraits/women-in-tech/card_049.png new file mode 100644 index 0000000..62ff29b Binary files /dev/null and b/assets/portraits/women-in-tech/card_049.png differ diff --git a/assets/portraits/women-in-tech/card_050.png b/assets/portraits/women-in-tech/card_050.png new file mode 100644 index 0000000..036352e Binary files /dev/null and b/assets/portraits/women-in-tech/card_050.png differ diff --git a/data/women-references.json b/data/women-references.json new file mode 100644 index 0000000..d9e681e --- /dev/null +++ b/data/women-references.json @@ -0,0 +1,796 @@ +[ + { + "number": 1, + "slug": "ada-lovelace", + "name": "Ada Lovelace", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/8/87/Ada_Lovelace.jpg/1280px-Ada_Lovelace.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Margaret Sarah Carpenter", + "license": "Public domain", + "licenseUrl": "", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Ada_Lovelace.jpg", + "file": "Ada Lovelace.jpg", + "qid": "Q7259", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "English mathematician (1815–1852)" + }, + { + "number": 2, + "slug": "grace-hopper", + "name": "Grace Hopper", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/a/ad/Commodore_Grace_M._Hopper%2C_USN_%28covered%29.jpg/1280px-Commodore_Grace_M._Hopper%2C_USN_%28covered%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "James S. Davis", + "license": "Public domain", + "licenseUrl": "", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Commodore_Grace_M._Hopper,_USN_(covered).jpg", + "file": "Commodore Grace M. Hopper, USN (covered).jpg", + "qid": "Q11641", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American computer scientist and United States Navy officer (1906–1992)" + }, + { + "number": 3, + "slug": "margaret-hamilton", + "name": "Margaret Hamilton", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/2/21/Margaret_Hamilton_1966.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Press-materials are presumed to have been authored by the producers of the Broadway production of Goldilocks.", + "license": "Public domain", + "licenseUrl": "", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Margaret_Hamilton_1966.jpg", + "file": "Margaret Hamilton 1966.jpg", + "qid": "Q257756", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American film character actress (1902–1985)" + }, + { + "number": 4, + "slug": "katherine-johnson", + "name": "Katherine Johnson", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/6/6d/Katherine_Johnson_1983.jpg/1280px-Katherine_Johnson_1983.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "NASA", + "license": "Public domain", + "licenseUrl": "", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Katherine_Johnson_1983.jpg", + "file": "Katherine Johnson 1983.jpg", + "qid": "Q11740", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "African-American mathematician" + }, + { + "number": 5, + "slug": "dorothy-vaughan", + "name": "Dorothy Vaughan", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/b/b6/Dorothy_Johnson_Vaughan.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Unknown authorUnknown author", + "license": "Public domain", + "licenseUrl": "", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Dorothy_Johnson_Vaughan.jpg", + "file": "Dorothy Johnson Vaughan.jpg", + "qid": "Q22915129", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "African-American mathematician" + }, + { + "number": 6, + "slug": "jean-bartik", + "name": "Jean Bartik", + "ref": null, + "candidates": [], + "rejected": "Wikidata P18 is \"Two women operating ENIAC\", a wide room shot where no face is legible. Conditioning on it would invent one." + }, + { + "number": 7, + "slug": "betty-holberton", + "name": "Betty Holberton", + "ref": null, + "candidates": [], + "rejected": "The only Commons photograph is a small, low-resolution profile at the ENIAC panels. Not enough face to paint a likeness from without guessing." + }, + { + "number": 8, + "slug": "kathleen-booth", + "name": "Kathleen Booth", + "ref": null, + "candidates": [], + "rejected": "No freely licensed photograph found. Wikipedia carries no portrait." + }, + { + "number": 9, + "slug": "mary-allen-wilkes", + "name": "Mary Allen Wilkes", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/b/be/Mary_Allen_Wilkes_Portrait.jpeg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "unknown (work for hire)", + "license": "CC0", + "licenseUrl": "http://creativecommons.org/publicdomain/zero/1.0/deed.en", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Mary_Allen_Wilkes_Portrait.jpeg", + "file": "Mary Allen Wilkes Portrait.jpeg", + "qid": "Q16484", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American computer scientist" + }, + { + "number": 10, + "slug": "erna-schneider-hoover", + "name": "Erna Schneider Hoover", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/9/9a/Erna_Schneider_1944_yearbook_photo.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Unknown authorUnknown author", + "license": "Public domain", + "licenseUrl": "", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Erna_Schneider_1944_yearbook_photo.jpg", + "file": "Erna Schneider 1944 yearbook photo.jpg", + "qid": "Q41624", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American mathematician" + }, + { + "number": 11, + "slug": "elizabeth-feinler", + "name": "Elizabeth Feinler", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/6/6d/ElizabethFeinler-2011.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Elizabeth Feinler", + "license": "CC BY-SA 3.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/3.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:ElizabethFeinler-2011.jpg", + "file": "ElizabethFeinler-2011.jpg", + "qid": "Q7534", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American information scientist (born 1931)" + }, + { + "number": 12, + "slug": "stephanie-shirley", + "name": "Stephanie Shirley", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/e/e1/Dame_Stephanie_Shirley_-_2013.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Lynn Hart", + "license": "CC BY 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Dame_Stephanie_Shirley_-_2013.jpg", + "file": "Dame Stephanie Shirley - 2013.jpg", + "qid": "Q214661", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "British businesswoman and philanthropist (1933–2025)" + }, + { + "number": 13, + "slug": "joan-clarke", + "name": "Joan Clarke", + "ref": null, + "candidates": [], + "rejected": "Commons search returned a photograph of the house at 193 Rosendale Road that carries her blue plaque. Her Bletchley-era portraits are not freely licensed." + }, + { + "number": 14, + "slug": "elizebeth-smith-friedman", + "name": "Elizebeth Smith Friedman", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/1/13/E_friedman_pic.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Unknown", + "license": "Public domain", + "licenseUrl": "", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:E_friedman_pic.jpg", + "file": "E friedman pic.jpg", + "qid": "Q276090", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American cryptanalyst and author (1892-1980)" + }, + { + "number": 15, + "slug": "hedy-lamarr", + "name": "Hedy Lamarr", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/8/83/Hedy_Lamarr_Publicity_Photo_for_The_Heavenly_Body_1944.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Unknown authorUnknown author", + "license": "Public domain", + "licenseUrl": "", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Hedy_Lamarr_Publicity_Photo_for_The_Heavenly_Body_1944.jpg", + "file": "Hedy Lamarr Publicity Photo for The Heavenly Body 1944.jpg", + "qid": "Q49034", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Austrian-American actress and co-inventor of an early technique for spread spectrum communications and frequency hopping (1914-2000)" + }, + { + "number": 16, + "slug": "frances-allen", + "name": "Frances Allen", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/1/15/Allen_mg_2528-3750K-b.jpg/1280px-Allen_mg_2528-3750K-b.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Rama", + "license": "CC BY-SA 2.0 fr", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/2.0/fr/deed.en", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Allen_mg_2528-3750K-b.jpg", + "file": "Allen mg 2528-3750K-b.jpg", + "qid": "Q9602", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American computer scientist (1932–2020)" + }, + { + "number": 17, + "slug": "barbara-liskov", + "name": "Barbara Liskov", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/c/cc/Turing_Centenary_Celebration_Liskov.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Dennis Hamilton from Seattle, Washington, USA", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Turing_Centenary_Celebration_Liskov.jpg", + "file": "Turing Centenary Celebration Liskov.jpg", + "qid": "Q16080922", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American computer scientist" + }, + { + "number": 18, + "slug": "shafi-goldwasser", + "name": "Shafi Goldwasser", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/8/84/Shafi_Goldwasser.JPG?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Weizmann Institute of Science", + "license": "Public domain", + "licenseUrl": "", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Shafi_Goldwasser.JPG", + "file": "Shafi Goldwasser.JPG", + "qid": "Q11609", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American computer scientist" + }, + { + "number": 19, + "slug": "karen-sparck-jones", + "name": "Karen Spärck Jones", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/a/af/Karen_Sp%C3%A4rck.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "en:user:Markus Kuhn", + "license": "CC BY 2.5", + "licenseUrl": "https://creativecommons.org/licenses/by/2.5", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Karen_Sp%C3%A4rck.jpg", + "file": "Karen Spärck.jpg", + "qid": "Q14254", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "British computer scientist (1935–2007)" + }, + { + "number": 20, + "slug": "cynthia-dwork", + "name": "Cynthia Dwork", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/6/6a/Cynthia_Dwork_lectures_at_Harvard_Kennedy_School.jpg/1280px-Cynthia_Dwork_lectures_at_Harvard_Kennedy_School.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Edmond J. Safra Center for Ethics", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Cynthia_Dwork_lectures_at_Harvard_Kennedy_School.jpg", + "file": "Cynthia Dwork lectures at Harvard Kennedy School.jpg", + "qid": "Q11580", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American computer scientist" + }, + { + "number": 21, + "slug": "anita-borg", + "name": "Anita Borg", + "ref": null, + "candidates": [], + "rejected": "Commons search returned a screenshot of her Wikidata item structure, a diagram." + }, + { + "number": 22, + "slug": "lynn-conway", + "name": "Lynn Conway", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/5/50/Lynn_Conway_July_2006.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Charles Rogers", + "license": "CC BY-SA 2.5", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/2.5", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Lynn_Conway_July_2006.jpg", + "file": "Lynn Conway July 2006.jpg", + "qid": "Q11490", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American computer scientist and electrical engineer (1938–2024)" + }, + { + "number": 23, + "slug": "sophie-wilson", + "name": "Sophie Wilson", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/b/b3/Sophie_Wilson.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Chris Monk", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Sophie_Wilson.jpg", + "file": "Sophie Wilson.jpg", + "qid": "Q16489", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "British computer scientist (born 1957)" + }, + { + "number": 24, + "slug": "adele-goldberg", + "name": "Adele Goldberg", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/3/36/Adele_Goldberg_at_PyCon_2007.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Terry Hancock", + "license": "CC BY-SA 2.5", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/2.5", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Adele_Goldberg_at_PyCon_2007.jpg", + "file": "Adele Goldberg at PyCon 2007.jpg", + "qid": "Q11605", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American computer scientist (born 1945)" + }, + { + "number": 25, + "slug": "susan-kare", + "name": "Susan Kare", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/8/88/SusanKare2019photo.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Cooper Hewitt", + "license": "CC BY 3.0", + "licenseUrl": "https://creativecommons.org/licenses/by/3.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:SusanKare2019photo.jpg", + "file": "SusanKare2019photo.jpg", + "qid": "Q48971", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American artist and graphic designer (born 1954)" + }, + { + "number": 26, + "slug": "limor-fried", + "name": "Limor Fried", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/a/af/Limor_Fried_TC2013.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "TechCrunch", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Limor_Fried_TC2013.jpg", + "file": "Limor Fried TC2013.jpg", + "qid": "Q538651", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "electrical engineer" + }, + { + "number": 27, + "slug": "mitchell-baker", + "name": "Mitchell Baker", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/8/8a/Mitchell_Baker.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "James Duncan Davidson/O'Reilly Media", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Mitchell_Baker.jpg", + "file": "Mitchell Baker.jpg", + "qid": null, + "via": "commons-search" + }, + "candidates": [ + "Mitchell Baker.jpg", + "Mitchell Baker-20081018.jpg", + "Mitchell Baker (1).jpg", + "Mitchell Baker (2).jpg", + "Mitchell Baker (3).jpg", + "Mitchell Baker (4).jpg", + "Mitchell Baker, Jonathan Schwartz (50185854).jpg", + "Mitchell Baker (50185859).jpg", + "Mitchell Baker 2009 crop.png", + "Mitchell Baker Michele Jawondo UNCTAD eWeek – 5 December 2023 04.jpg" + ] + }, + { + "number": 28, + "slug": "audrey-tang", + "name": "Audrey Tang", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/d/d3/AudreyTang060527.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "AudreyTang", + "license": "CC0", + "licenseUrl": "http://creativecommons.org/publicdomain/zero/1.0/deed.en", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:AudreyTang060527.jpg", + "file": "AudreyTang060527.jpg", + "qid": "Q15025", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Taiwanese activist, politician and free software programmer" + }, + { + "number": 29, + "slug": "karen-sandler", + "name": "Karen Sandler", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/a/a6/Karen_Sandler_-_Swathanthra_2017_-_DSC_9737.jpg/1280px-Karen_Sandler_-_Swathanthra_2017_-_DSC_9737.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Ranjithsiji", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Karen_Sandler_-_Swathanthra_2017_-_DSC_9737.jpg", + "file": "Karen Sandler - Swathanthra 2017 - DSC 9737.jpg", + "qid": "Q6370015", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Executive Director of Software Freedom Conservancy and American lawyer" + }, + { + "number": 30, + "slug": "danese-cooper", + "name": "Danese Cooper", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/b/bf/Wikimedia_Foundation_Danese_Cooper_Sept_2010.jpg/1280px-Wikimedia_Foundation_Danese_Cooper_Sept_2010.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Lane Hartwell", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Wikimedia_Foundation_Danese_Cooper_Sept_2010.jpg", + "file": "Wikimedia Foundation Danese Cooper Sept 2010.jpg", + "qid": "Q11493", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American advocate of open source software" + }, + { + "number": 31, + "slug": "allison-randal", + "name": "Allison Randal", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/1/16/Allison_Randal.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "The original uploader was Cjcollier at English Wikipedia.", + "license": "CC BY-SA 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Allison_Randal.jpg", + "file": "Allison Randal.jpg", + "qid": "Q3296638", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Computer writer" + }, + { + "number": 32, + "slug": "valerie-aurora", + "name": "Valerie Aurora", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/f/f5/Valerie_Aurora_headshot.jpg/1280px-Valerie_Aurora_headshot.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Photographer unknown, commissioned by Valerie Aurora", + "license": "CC BY 3.0", + "licenseUrl": "https://creativecommons.org/licenses/by/3.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Valerie_Aurora_headshot.jpg", + "file": "Valerie Aurora headshot.jpg", + "qid": "Q7536", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American computer scientist and activist" + }, + { + "number": 33, + "slug": "coraline-ada-ehmke", + "name": "Coraline Ada Ehmke", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/a/af/Coraline_Ada_Ehmke.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Coraline Ada Ehmke", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Coraline_Ada_Ehmke.jpg", + "file": "Coraline Ada Ehmke.jpg", + "qid": "Q24192535", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "software developer and open source community activist" + }, + { + "number": 34, + "slug": "sumana-harihareswara", + "name": "Sumana Harihareswara", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/9/92/Sumana_Harihareswara_021_-_Berlin_2011.jpg/1280px-Sumana_Harihareswara_021_-_Berlin_2011.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Guillaume Paumier", + "license": "CC BY 3.0", + "licenseUrl": "https://creativecommons.org/licenses/by/3.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Sumana_Harihareswara_021_-_Berlin_2011.jpg", + "file": "Sumana Harihareswara 021 - Berlin 2011.jpg", + "qid": null, + "via": "commons-search" + }, + "candidates": [ + "Sumana Harihareswara 021 - Berlin 2011.jpg", + "Sumana Harihareswara.jpg", + "Sumana Harihareswara 5302765.JPG", + "Sumana Harihareswara speaking at WikiConference USA in 2014.JPG" + ] + }, + { + "number": 35, + "slug": "latanya-sweeney", + "name": "Latanya Sweeney", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/b/b6/Latanya_Arvette_Sweeney.jpg/1280px-Latanya_Arvette_Sweeney.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "'arcticpenguin'", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Latanya_Arvette_Sweeney.jpg", + "file": "Latanya Arvette Sweeney.jpg", + "qid": "Q16499775", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "computer scientist" + }, + { + "number": 36, + "slug": "jessica-mckellar", + "name": "Jessica McKellar", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/d/d1/Jessica_McKellar_selfportrait_BW.jpeg/1280px-Jessica_McKellar_selfportrait_BW.jpeg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Jessica McKellar", + "license": "CC BY-SA 3.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/3.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Jessica_McKellar_selfportrait_BW.jpeg", + "file": "Jessica McKellar selfportrait BW.jpeg", + "qid": "Q19667922", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American software developer, engineering manager, and author" + }, + { + "number": 37, + "slug": "carol-willing", + "name": "Carol Willing", + "ref": { + "url": "https://avatars.githubusercontent.com/u/2680980?v=4&s=460", + "credit": "Carol Willing", + "license": "supplied", + "sourceUrl": "https://github.com/willingc", + "note": "Profile photograph; account verified against their real name.", + "via": "override" + }, + "candidates": [] + }, + { + "number": 38, + "slug": "julia-evans", + "name": "Julia Evans", + "ref": null, + "candidates": [], + "rejected": "Commons search returned \"Julia Evans Reed.jpg\", a different person, and her GitHub avatar is a cartoon star rather than a photograph." + }, + { + "number": 39, + "slug": "rachel-andrew", + "name": "Rachel Andrew", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/c/cd/Rachel_Andrew.jpg/1280px-Rachel_Andrew.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Drew McLellan", + "license": "CC BY-SA 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Rachel_Andrew.jpg", + "file": "Rachel Andrew.jpg", + "qid": null, + "via": "commons-search" + }, + "candidates": [ + "Rachel Andrew.jpg", + "Rachel Andrew- CSS Grid Layout at -AEASF 2015.jpg", + "Rachel Andrew talks.jpg" + ] + }, + { + "number": 40, + "slug": "jen-simmons", + "name": "Jen Simmons", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/0/06/Jen_Simmons_AEACHI_%28cropped%29.jpeg/1280px-Jen_Simmons_AEACHI_%28cropped%29.jpeg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Jeffrey Zeldman", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Jen_Simmons_AEACHI_(cropped).jpeg", + "file": "Jen Simmons AEACHI (cropped).jpeg", + "qid": "Q47171897", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "web designer and developer" + }, + { + "number": 41, + "slug": "lea-verou", + "name": "Lea Verou", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/e/e2/Lea_Verou%2C_speaking_at_the_Fonteers_2011_conference.jpg/1280px-Lea_Verou%2C_speaking_at_the_Fonteers_2011_conference.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "OutofTheBoxThinker", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Lea_Verou,_speaking_at_the_Fonteers_2011_conference.jpg", + "file": "Lea Verou, speaking at the Fonteers 2011 conference.jpg", + "qid": null, + "via": "commons-search" + }, + "candidates": [ + "Lea Verou, speaking at the Fonteers 2011 conference.jpg", + "Lea-verou.jpg" + ] + }, + { + "number": 42, + "slug": "sarah-drasner", + "name": "Sarah Drasner", + "ref": { + "url": "https://avatars.githubusercontent.com/u/2281088?v=4&s=460", + "credit": "Sarah Drasner", + "license": "supplied", + "sourceUrl": "https://github.com/sdras", + "note": "Profile photograph; account verified against their real name.", + "via": "override" + }, + "candidates": [] + }, + { + "number": 43, + "slug": "leonie-watson", + "name": "Léonie Watson", + "ref": null, + "candidates": [], + "rejected": "Her GitHub avatar is the tink logo and her about page serves no photograph." + }, + { + "number": 44, + "slug": "charity-majors", + "name": "Charity Majors", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/e/e7/Charity_Majors.png/1280px-Charity_Majors.png?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Attribution: CityTV.nl", + "license": "CC BY 3.0", + "licenseUrl": "https://creativecommons.org/licenses/by/3.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Charity_Majors.png", + "file": "Charity Majors.png", + "qid": "Q108913571", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American software engineer" + }, + { + "number": 45, + "slug": "liz-fong-jones", + "name": "Liz Fong-Jones", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/6/64/Liz_Fong-Jones_at_All_Things_Open_workshop.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "All Things Open", + "license": "CC BY 3.0", + "licenseUrl": "https://creativecommons.org/licenses/by/3.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Liz_Fong-Jones_at_All_Things_Open_workshop.jpg", + "file": "Liz Fong-Jones at All Things Open workshop.jpg", + "qid": "Q88326140", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American site reliability engineer and developer advocate" + }, + { + "number": 46, + "slug": "jessie-frazelle", + "name": "Jessie Frazelle", + "ref": { + "url": "https://avatars.githubusercontent.com/u/1445228?v=4&s=460", + "credit": "Jess Frazelle", + "license": "supplied", + "sourceUrl": "https://github.com/jessfraz", + "note": "Profile photograph; account verified against their real name.", + "via": "override" + }, + "candidates": [] + }, + { + "number": 47, + "slug": "camille-fournier", + "name": "Camille Fournier", + "ref": { + "url": "https://avatars.githubusercontent.com/u/607890?v=4&s=460", + "credit": "Camille Fournier", + "license": "supplied", + "sourceUrl": "https://github.com/skamille", + "note": "Profile photograph; account verified against their real name.", + "via": "override" + }, + "candidates": [] + }, + { + "number": 48, + "slug": "timnit-gebru", + "name": "Timnit Gebru", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/6/6d/Timnit_Gebru_crop.jpg/1280px-Timnit_Gebru_crop.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "TechCrunch", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Timnit_Gebru_crop.jpg", + "file": "Timnit Gebru crop.jpg", + "qid": "Q59753117", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "computer scientist, specialising in AI ethics" + }, + { + "number": 49, + "slug": "joy-buolamwini", + "name": "Joy Buolamwini", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/c/c0/Joy_Buolamwini%2C_2018_%28cropped%29.jpg/1280px-Joy_Buolamwini%2C_2018_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Niccolò Caranti", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Joy_Buolamwini,_2018_(cropped).jpg", + "file": "Joy Buolamwini, 2018 (cropped).jpg", + "qid": "Q31359093", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American computer scientist and digital activist" + }, + { + "number": 50, + "slug": "margaret-mitchell", + "name": "Margaret Mitchell", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/7/75/MargaretMitchell2022.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "AI4LAM", + "license": "CC BY 3.0", + "licenseUrl": "https://creativecommons.org/licenses/by/3.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:MargaretMitchell2022.jpg", + "file": "MargaretMitchell2022.jpg", + "qid": "Q50346871", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "researcher, natural language processing" + } +] diff --git a/package.json b/package.json index 2bcecdd..2473b5e 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,11 @@ "gods": "node scripts/gods-cards.mjs", "gods:validate": "node scripts/gods-cards.mjs validate", "gods:render": "node scripts/gods-cards.mjs render", - "gods:refs": "node scripts/gods-refs.mjs" + "gods:refs": "node scripts/gods-refs.mjs", + "women": "node scripts/women-cards.mjs", + "women:validate": "node scripts/women-cards.mjs validate", + "women:render": "node scripts/women-cards.mjs render", + "women:refs": "node scripts/women-refs.mjs" }, "dependencies": { "@libsql/client": "^0.17.4", diff --git a/public/cards/women/001-ada-lovelace-back.png b/public/cards/women/001-ada-lovelace-back.png new file mode 100644 index 0000000..6cb8ad1 Binary files /dev/null and b/public/cards/women/001-ada-lovelace-back.png differ diff --git a/public/cards/women/001-ada-lovelace-front.png b/public/cards/women/001-ada-lovelace-front.png new file mode 100644 index 0000000..b98c946 Binary files /dev/null and b/public/cards/women/001-ada-lovelace-front.png differ diff --git a/public/cards/women/002-grace-hopper-back.png b/public/cards/women/002-grace-hopper-back.png new file mode 100644 index 0000000..3668cb4 Binary files /dev/null and b/public/cards/women/002-grace-hopper-back.png differ diff --git a/public/cards/women/002-grace-hopper-front.png b/public/cards/women/002-grace-hopper-front.png new file mode 100644 index 0000000..7f15ff4 Binary files /dev/null and b/public/cards/women/002-grace-hopper-front.png differ diff --git a/public/cards/women/003-margaret-hamilton-back.png b/public/cards/women/003-margaret-hamilton-back.png new file mode 100644 index 0000000..ae5f19f Binary files /dev/null and b/public/cards/women/003-margaret-hamilton-back.png differ diff --git a/public/cards/women/003-margaret-hamilton-front.png b/public/cards/women/003-margaret-hamilton-front.png new file mode 100644 index 0000000..b4d5fdb Binary files /dev/null and b/public/cards/women/003-margaret-hamilton-front.png differ diff --git a/public/cards/women/004-katherine-johnson-back.png b/public/cards/women/004-katherine-johnson-back.png new file mode 100644 index 0000000..4546b72 Binary files /dev/null and b/public/cards/women/004-katherine-johnson-back.png differ diff --git a/public/cards/women/004-katherine-johnson-front.png b/public/cards/women/004-katherine-johnson-front.png new file mode 100644 index 0000000..fe846cc Binary files /dev/null and b/public/cards/women/004-katherine-johnson-front.png differ diff --git a/public/cards/women/005-dorothy-vaughan-back.png b/public/cards/women/005-dorothy-vaughan-back.png new file mode 100644 index 0000000..203f9b2 Binary files /dev/null and b/public/cards/women/005-dorothy-vaughan-back.png differ diff --git a/public/cards/women/005-dorothy-vaughan-front.png b/public/cards/women/005-dorothy-vaughan-front.png new file mode 100644 index 0000000..a9309f5 Binary files /dev/null and b/public/cards/women/005-dorothy-vaughan-front.png differ diff --git a/public/cards/women/006-jean-bartik-back.png b/public/cards/women/006-jean-bartik-back.png new file mode 100644 index 0000000..1a6152d Binary files /dev/null and b/public/cards/women/006-jean-bartik-back.png differ diff --git a/public/cards/women/006-jean-bartik-front.png b/public/cards/women/006-jean-bartik-front.png new file mode 100644 index 0000000..b51a471 Binary files /dev/null and b/public/cards/women/006-jean-bartik-front.png differ diff --git a/public/cards/women/007-betty-holberton-back.png b/public/cards/women/007-betty-holberton-back.png new file mode 100644 index 0000000..b76fbf7 Binary files /dev/null and b/public/cards/women/007-betty-holberton-back.png differ diff --git a/public/cards/women/007-betty-holberton-front.png b/public/cards/women/007-betty-holberton-front.png new file mode 100644 index 0000000..3cc35aa Binary files /dev/null and b/public/cards/women/007-betty-holberton-front.png differ diff --git a/public/cards/women/008-kathleen-booth-back.png b/public/cards/women/008-kathleen-booth-back.png new file mode 100644 index 0000000..54113af Binary files /dev/null and b/public/cards/women/008-kathleen-booth-back.png differ diff --git a/public/cards/women/008-kathleen-booth-front.png b/public/cards/women/008-kathleen-booth-front.png new file mode 100644 index 0000000..00e80a1 Binary files /dev/null and b/public/cards/women/008-kathleen-booth-front.png differ diff --git a/public/cards/women/009-mary-allen-wilkes-back.png b/public/cards/women/009-mary-allen-wilkes-back.png new file mode 100644 index 0000000..6014f6c Binary files /dev/null and b/public/cards/women/009-mary-allen-wilkes-back.png differ diff --git a/public/cards/women/009-mary-allen-wilkes-front.png b/public/cards/women/009-mary-allen-wilkes-front.png new file mode 100644 index 0000000..56fed4b Binary files /dev/null and b/public/cards/women/009-mary-allen-wilkes-front.png differ diff --git a/public/cards/women/010-erna-schneider-hoover-back.png b/public/cards/women/010-erna-schneider-hoover-back.png new file mode 100644 index 0000000..5dae338 Binary files /dev/null and b/public/cards/women/010-erna-schneider-hoover-back.png differ diff --git a/public/cards/women/010-erna-schneider-hoover-front.png b/public/cards/women/010-erna-schneider-hoover-front.png new file mode 100644 index 0000000..4fffe1f Binary files /dev/null and b/public/cards/women/010-erna-schneider-hoover-front.png differ diff --git a/public/cards/women/011-elizabeth-feinler-back.png b/public/cards/women/011-elizabeth-feinler-back.png new file mode 100644 index 0000000..fb69f02 Binary files /dev/null and b/public/cards/women/011-elizabeth-feinler-back.png differ diff --git a/public/cards/women/011-elizabeth-feinler-front.png b/public/cards/women/011-elizabeth-feinler-front.png new file mode 100644 index 0000000..cb797b1 Binary files /dev/null and b/public/cards/women/011-elizabeth-feinler-front.png differ diff --git a/public/cards/women/012-stephanie-shirley-back.png b/public/cards/women/012-stephanie-shirley-back.png new file mode 100644 index 0000000..5138dd1 Binary files /dev/null and b/public/cards/women/012-stephanie-shirley-back.png differ diff --git a/public/cards/women/012-stephanie-shirley-front.png b/public/cards/women/012-stephanie-shirley-front.png new file mode 100644 index 0000000..d445dcc Binary files /dev/null and b/public/cards/women/012-stephanie-shirley-front.png differ diff --git a/public/cards/women/013-joan-clarke-back.png b/public/cards/women/013-joan-clarke-back.png new file mode 100644 index 0000000..b98c12b Binary files /dev/null and b/public/cards/women/013-joan-clarke-back.png differ diff --git a/public/cards/women/013-joan-clarke-front.png b/public/cards/women/013-joan-clarke-front.png new file mode 100644 index 0000000..c1bc5e2 Binary files /dev/null and b/public/cards/women/013-joan-clarke-front.png differ diff --git a/public/cards/women/014-elizebeth-smith-friedman-back.png b/public/cards/women/014-elizebeth-smith-friedman-back.png new file mode 100644 index 0000000..d51c862 Binary files /dev/null and b/public/cards/women/014-elizebeth-smith-friedman-back.png differ diff --git a/public/cards/women/014-elizebeth-smith-friedman-front.png b/public/cards/women/014-elizebeth-smith-friedman-front.png new file mode 100644 index 0000000..082515f Binary files /dev/null and b/public/cards/women/014-elizebeth-smith-friedman-front.png differ diff --git a/public/cards/women/015-hedy-lamarr-back.png b/public/cards/women/015-hedy-lamarr-back.png new file mode 100644 index 0000000..9c10e99 Binary files /dev/null and b/public/cards/women/015-hedy-lamarr-back.png differ diff --git a/public/cards/women/015-hedy-lamarr-front.png b/public/cards/women/015-hedy-lamarr-front.png new file mode 100644 index 0000000..a042184 Binary files /dev/null and b/public/cards/women/015-hedy-lamarr-front.png differ diff --git a/public/cards/women/016-frances-allen-back.png b/public/cards/women/016-frances-allen-back.png new file mode 100644 index 0000000..2268b23 Binary files /dev/null and b/public/cards/women/016-frances-allen-back.png differ diff --git a/public/cards/women/016-frances-allen-front.png b/public/cards/women/016-frances-allen-front.png new file mode 100644 index 0000000..73c91d4 Binary files /dev/null and b/public/cards/women/016-frances-allen-front.png differ diff --git a/public/cards/women/017-barbara-liskov-back.png b/public/cards/women/017-barbara-liskov-back.png new file mode 100644 index 0000000..7e27321 Binary files /dev/null and b/public/cards/women/017-barbara-liskov-back.png differ diff --git a/public/cards/women/017-barbara-liskov-front.png b/public/cards/women/017-barbara-liskov-front.png new file mode 100644 index 0000000..0544838 Binary files /dev/null and b/public/cards/women/017-barbara-liskov-front.png differ diff --git a/public/cards/women/018-shafi-goldwasser-back.png b/public/cards/women/018-shafi-goldwasser-back.png new file mode 100644 index 0000000..6834bff Binary files /dev/null and b/public/cards/women/018-shafi-goldwasser-back.png differ diff --git a/public/cards/women/018-shafi-goldwasser-front.png b/public/cards/women/018-shafi-goldwasser-front.png new file mode 100644 index 0000000..ac7ff8f Binary files /dev/null and b/public/cards/women/018-shafi-goldwasser-front.png differ diff --git a/public/cards/women/019-karen-sparck-jones-back.png b/public/cards/women/019-karen-sparck-jones-back.png new file mode 100644 index 0000000..e7b0ea2 Binary files /dev/null and b/public/cards/women/019-karen-sparck-jones-back.png differ diff --git a/public/cards/women/019-karen-sparck-jones-front.png b/public/cards/women/019-karen-sparck-jones-front.png new file mode 100644 index 0000000..4fcabc4 Binary files /dev/null and b/public/cards/women/019-karen-sparck-jones-front.png differ diff --git a/public/cards/women/020-cynthia-dwork-back.png b/public/cards/women/020-cynthia-dwork-back.png new file mode 100644 index 0000000..5219dc0 Binary files /dev/null and b/public/cards/women/020-cynthia-dwork-back.png differ diff --git a/public/cards/women/020-cynthia-dwork-front.png b/public/cards/women/020-cynthia-dwork-front.png new file mode 100644 index 0000000..03d9c35 Binary files /dev/null and b/public/cards/women/020-cynthia-dwork-front.png differ diff --git a/public/cards/women/021-anita-borg-back.png b/public/cards/women/021-anita-borg-back.png new file mode 100644 index 0000000..118f5f1 Binary files /dev/null and b/public/cards/women/021-anita-borg-back.png differ diff --git a/public/cards/women/021-anita-borg-front.png b/public/cards/women/021-anita-borg-front.png new file mode 100644 index 0000000..a65c562 Binary files /dev/null and b/public/cards/women/021-anita-borg-front.png differ diff --git a/public/cards/women/022-lynn-conway-back.png b/public/cards/women/022-lynn-conway-back.png new file mode 100644 index 0000000..ce495ad Binary files /dev/null and b/public/cards/women/022-lynn-conway-back.png differ diff --git a/public/cards/women/022-lynn-conway-front.png b/public/cards/women/022-lynn-conway-front.png new file mode 100644 index 0000000..ee115d2 Binary files /dev/null and b/public/cards/women/022-lynn-conway-front.png differ diff --git a/public/cards/women/023-sophie-wilson-back.png b/public/cards/women/023-sophie-wilson-back.png new file mode 100644 index 0000000..cb99b8f Binary files /dev/null and b/public/cards/women/023-sophie-wilson-back.png differ diff --git a/public/cards/women/023-sophie-wilson-front.png b/public/cards/women/023-sophie-wilson-front.png new file mode 100644 index 0000000..9b366d2 Binary files /dev/null and b/public/cards/women/023-sophie-wilson-front.png differ diff --git a/public/cards/women/024-adele-goldberg-back.png b/public/cards/women/024-adele-goldberg-back.png new file mode 100644 index 0000000..ed7651d Binary files /dev/null and b/public/cards/women/024-adele-goldberg-back.png differ diff --git a/public/cards/women/024-adele-goldberg-front.png b/public/cards/women/024-adele-goldberg-front.png new file mode 100644 index 0000000..e4125b0 Binary files /dev/null and b/public/cards/women/024-adele-goldberg-front.png differ diff --git a/public/cards/women/025-susan-kare-back.png b/public/cards/women/025-susan-kare-back.png new file mode 100644 index 0000000..5753fb2 Binary files /dev/null and b/public/cards/women/025-susan-kare-back.png differ diff --git a/public/cards/women/025-susan-kare-front.png b/public/cards/women/025-susan-kare-front.png new file mode 100644 index 0000000..3d76fea Binary files /dev/null and b/public/cards/women/025-susan-kare-front.png differ diff --git a/public/cards/women/026-limor-fried-back.png b/public/cards/women/026-limor-fried-back.png new file mode 100644 index 0000000..b2d0d51 Binary files /dev/null and b/public/cards/women/026-limor-fried-back.png differ diff --git a/public/cards/women/026-limor-fried-front.png b/public/cards/women/026-limor-fried-front.png new file mode 100644 index 0000000..d92d8d9 Binary files /dev/null and b/public/cards/women/026-limor-fried-front.png differ diff --git a/public/cards/women/027-mitchell-baker-back.png b/public/cards/women/027-mitchell-baker-back.png new file mode 100644 index 0000000..5fd5340 Binary files /dev/null and b/public/cards/women/027-mitchell-baker-back.png differ diff --git a/public/cards/women/027-mitchell-baker-front.png b/public/cards/women/027-mitchell-baker-front.png new file mode 100644 index 0000000..b745131 Binary files /dev/null and b/public/cards/women/027-mitchell-baker-front.png differ diff --git a/public/cards/women/028-audrey-tang-back.png b/public/cards/women/028-audrey-tang-back.png new file mode 100644 index 0000000..e98a8ff Binary files /dev/null and b/public/cards/women/028-audrey-tang-back.png differ diff --git a/public/cards/women/028-audrey-tang-front.png b/public/cards/women/028-audrey-tang-front.png new file mode 100644 index 0000000..82bf6bf Binary files /dev/null and b/public/cards/women/028-audrey-tang-front.png differ diff --git a/public/cards/women/029-karen-sandler-back.png b/public/cards/women/029-karen-sandler-back.png new file mode 100644 index 0000000..455a23d Binary files /dev/null and b/public/cards/women/029-karen-sandler-back.png differ diff --git a/public/cards/women/029-karen-sandler-front.png b/public/cards/women/029-karen-sandler-front.png new file mode 100644 index 0000000..3c16215 Binary files /dev/null and b/public/cards/women/029-karen-sandler-front.png differ diff --git a/public/cards/women/030-danese-cooper-back.png b/public/cards/women/030-danese-cooper-back.png new file mode 100644 index 0000000..5b0b609 Binary files /dev/null and b/public/cards/women/030-danese-cooper-back.png differ diff --git a/public/cards/women/030-danese-cooper-front.png b/public/cards/women/030-danese-cooper-front.png new file mode 100644 index 0000000..fb812dd Binary files /dev/null and b/public/cards/women/030-danese-cooper-front.png differ diff --git a/public/cards/women/031-allison-randal-back.png b/public/cards/women/031-allison-randal-back.png new file mode 100644 index 0000000..1eb3f5d Binary files /dev/null and b/public/cards/women/031-allison-randal-back.png differ diff --git a/public/cards/women/031-allison-randal-front.png b/public/cards/women/031-allison-randal-front.png new file mode 100644 index 0000000..573e28c Binary files /dev/null and b/public/cards/women/031-allison-randal-front.png differ diff --git a/public/cards/women/032-valerie-aurora-back.png b/public/cards/women/032-valerie-aurora-back.png new file mode 100644 index 0000000..3fa54c8 Binary files /dev/null and b/public/cards/women/032-valerie-aurora-back.png differ diff --git a/public/cards/women/032-valerie-aurora-front.png b/public/cards/women/032-valerie-aurora-front.png new file mode 100644 index 0000000..b39907f Binary files /dev/null and b/public/cards/women/032-valerie-aurora-front.png differ diff --git a/public/cards/women/033-coraline-ada-ehmke-back.png b/public/cards/women/033-coraline-ada-ehmke-back.png new file mode 100644 index 0000000..24e9ab5 Binary files /dev/null and b/public/cards/women/033-coraline-ada-ehmke-back.png differ diff --git a/public/cards/women/033-coraline-ada-ehmke-front.png b/public/cards/women/033-coraline-ada-ehmke-front.png new file mode 100644 index 0000000..32abc8c Binary files /dev/null and b/public/cards/women/033-coraline-ada-ehmke-front.png differ diff --git a/public/cards/women/034-sumana-harihareswara-back.png b/public/cards/women/034-sumana-harihareswara-back.png new file mode 100644 index 0000000..5277486 Binary files /dev/null and b/public/cards/women/034-sumana-harihareswara-back.png differ diff --git a/public/cards/women/034-sumana-harihareswara-front.png b/public/cards/women/034-sumana-harihareswara-front.png new file mode 100644 index 0000000..df121df Binary files /dev/null and b/public/cards/women/034-sumana-harihareswara-front.png differ diff --git a/public/cards/women/035-latanya-sweeney-back.png b/public/cards/women/035-latanya-sweeney-back.png new file mode 100644 index 0000000..9924a48 Binary files /dev/null and b/public/cards/women/035-latanya-sweeney-back.png differ diff --git a/public/cards/women/035-latanya-sweeney-front.png b/public/cards/women/035-latanya-sweeney-front.png new file mode 100644 index 0000000..1c6af57 Binary files /dev/null and b/public/cards/women/035-latanya-sweeney-front.png differ diff --git a/public/cards/women/036-jessica-mckellar-back.png b/public/cards/women/036-jessica-mckellar-back.png new file mode 100644 index 0000000..8544631 Binary files /dev/null and b/public/cards/women/036-jessica-mckellar-back.png differ diff --git a/public/cards/women/036-jessica-mckellar-front.png b/public/cards/women/036-jessica-mckellar-front.png new file mode 100644 index 0000000..89960b9 Binary files /dev/null and b/public/cards/women/036-jessica-mckellar-front.png differ diff --git a/public/cards/women/037-carol-willing-back.png b/public/cards/women/037-carol-willing-back.png new file mode 100644 index 0000000..10e9aa7 Binary files /dev/null and b/public/cards/women/037-carol-willing-back.png differ diff --git a/public/cards/women/037-carol-willing-front.png b/public/cards/women/037-carol-willing-front.png new file mode 100644 index 0000000..8683c54 Binary files /dev/null and b/public/cards/women/037-carol-willing-front.png differ diff --git a/public/cards/women/038-julia-evans-back.png b/public/cards/women/038-julia-evans-back.png new file mode 100644 index 0000000..0a3945c Binary files /dev/null and b/public/cards/women/038-julia-evans-back.png differ diff --git a/public/cards/women/038-julia-evans-front.png b/public/cards/women/038-julia-evans-front.png new file mode 100644 index 0000000..f66a359 Binary files /dev/null and b/public/cards/women/038-julia-evans-front.png differ diff --git a/public/cards/women/039-rachel-andrew-back.png b/public/cards/women/039-rachel-andrew-back.png new file mode 100644 index 0000000..5dce5f5 Binary files /dev/null and b/public/cards/women/039-rachel-andrew-back.png differ diff --git a/public/cards/women/039-rachel-andrew-front.png b/public/cards/women/039-rachel-andrew-front.png new file mode 100644 index 0000000..07c8c75 Binary files /dev/null and b/public/cards/women/039-rachel-andrew-front.png differ diff --git a/public/cards/women/040-jen-simmons-back.png b/public/cards/women/040-jen-simmons-back.png new file mode 100644 index 0000000..e2838d7 Binary files /dev/null and b/public/cards/women/040-jen-simmons-back.png differ diff --git a/public/cards/women/040-jen-simmons-front.png b/public/cards/women/040-jen-simmons-front.png new file mode 100644 index 0000000..181f443 Binary files /dev/null and b/public/cards/women/040-jen-simmons-front.png differ diff --git a/public/cards/women/041-lea-verou-back.png b/public/cards/women/041-lea-verou-back.png new file mode 100644 index 0000000..c3ed77b Binary files /dev/null and b/public/cards/women/041-lea-verou-back.png differ diff --git a/public/cards/women/041-lea-verou-front.png b/public/cards/women/041-lea-verou-front.png new file mode 100644 index 0000000..ee36cb6 Binary files /dev/null and b/public/cards/women/041-lea-verou-front.png differ diff --git a/public/cards/women/042-sarah-drasner-back.png b/public/cards/women/042-sarah-drasner-back.png new file mode 100644 index 0000000..e5ff900 Binary files /dev/null and b/public/cards/women/042-sarah-drasner-back.png differ diff --git a/public/cards/women/042-sarah-drasner-front.png b/public/cards/women/042-sarah-drasner-front.png new file mode 100644 index 0000000..ea3199b Binary files /dev/null and b/public/cards/women/042-sarah-drasner-front.png differ diff --git a/public/cards/women/043-leonie-watson-back.png b/public/cards/women/043-leonie-watson-back.png new file mode 100644 index 0000000..2de261f Binary files /dev/null and b/public/cards/women/043-leonie-watson-back.png differ diff --git a/public/cards/women/043-leonie-watson-front.png b/public/cards/women/043-leonie-watson-front.png new file mode 100644 index 0000000..87c40bb Binary files /dev/null and b/public/cards/women/043-leonie-watson-front.png differ diff --git a/public/cards/women/044-charity-majors-back.png b/public/cards/women/044-charity-majors-back.png new file mode 100644 index 0000000..9f01679 Binary files /dev/null and b/public/cards/women/044-charity-majors-back.png differ diff --git a/public/cards/women/044-charity-majors-front.png b/public/cards/women/044-charity-majors-front.png new file mode 100644 index 0000000..236dc65 Binary files /dev/null and b/public/cards/women/044-charity-majors-front.png differ diff --git a/public/cards/women/045-liz-fong-jones-back.png b/public/cards/women/045-liz-fong-jones-back.png new file mode 100644 index 0000000..95a9729 Binary files /dev/null and b/public/cards/women/045-liz-fong-jones-back.png differ diff --git a/public/cards/women/045-liz-fong-jones-front.png b/public/cards/women/045-liz-fong-jones-front.png new file mode 100644 index 0000000..b78b5cf Binary files /dev/null and b/public/cards/women/045-liz-fong-jones-front.png differ diff --git a/public/cards/women/046-jessie-frazelle-back.png b/public/cards/women/046-jessie-frazelle-back.png new file mode 100644 index 0000000..5974714 Binary files /dev/null and b/public/cards/women/046-jessie-frazelle-back.png differ diff --git a/public/cards/women/046-jessie-frazelle-front.png b/public/cards/women/046-jessie-frazelle-front.png new file mode 100644 index 0000000..a9c24ef Binary files /dev/null and b/public/cards/women/046-jessie-frazelle-front.png differ diff --git a/public/cards/women/047-camille-fournier-back.png b/public/cards/women/047-camille-fournier-back.png new file mode 100644 index 0000000..f1e768c Binary files /dev/null and b/public/cards/women/047-camille-fournier-back.png differ diff --git a/public/cards/women/047-camille-fournier-front.png b/public/cards/women/047-camille-fournier-front.png new file mode 100644 index 0000000..3955f0e Binary files /dev/null and b/public/cards/women/047-camille-fournier-front.png differ diff --git a/public/cards/women/048-timnit-gebru-back.png b/public/cards/women/048-timnit-gebru-back.png new file mode 100644 index 0000000..ef2d014 Binary files /dev/null and b/public/cards/women/048-timnit-gebru-back.png differ diff --git a/public/cards/women/048-timnit-gebru-front.png b/public/cards/women/048-timnit-gebru-front.png new file mode 100644 index 0000000..7fd96f0 Binary files /dev/null and b/public/cards/women/048-timnit-gebru-front.png differ diff --git a/public/cards/women/049-joy-buolamwini-back.png b/public/cards/women/049-joy-buolamwini-back.png new file mode 100644 index 0000000..d7a5320 Binary files /dev/null and b/public/cards/women/049-joy-buolamwini-back.png differ diff --git a/public/cards/women/049-joy-buolamwini-front.png b/public/cards/women/049-joy-buolamwini-front.png new file mode 100644 index 0000000..e9e26e0 Binary files /dev/null and b/public/cards/women/049-joy-buolamwini-front.png differ diff --git a/public/cards/women/050-margaret-mitchell-back.png b/public/cards/women/050-margaret-mitchell-back.png new file mode 100644 index 0000000..ab2878f Binary files /dev/null and b/public/cards/women/050-margaret-mitchell-back.png differ diff --git a/public/cards/women/050-margaret-mitchell-front.png b/public/cards/women/050-margaret-mitchell-front.png new file mode 100644 index 0000000..c3ba814 Binary files /dev/null and b/public/cards/women/050-margaret-mitchell-front.png differ diff --git a/scripts/women-cards.mjs b/scripts/women-cards.mjs new file mode 100644 index 0000000..aa86446 --- /dev/null +++ b/scripts/women-cards.mjs @@ -0,0 +1,666 @@ +#!/usr/bin/env node +/* + Women in Tech (Series Five) — end-to-end card pipeline. + + Same shape as scripts/security-pros.mjs. Every stage skips work that already + exists, so re-running is cheap and a failed batch can just be run again. + + portraits AI portrait art, one per professional -> assets/portraits-art/women-in-tech/ + render HTML template -> exact card PNGs -> dist/women/html/ + enhance image-to-image premium finish pass -> dist/women/enhanced/ (OPT-IN, see below) + publish pick the best face available -> public/cards/women/ + validate roster + asset sanity checks + all portraits -> render -> publish + + WHY `all` DOES NOT RUN `enhance` + -------------------------------- + On Series Two the image-to-image finish pass rewrote text on the card faces + despite the prompt forbidding it: it fabricated quotes attributed to living + people, invented stat panels that were not on the card, and misspelled a name. + Nothing in the pipeline catches that, because there is no OCR step — it was + found by eye, after the art shipped. + + This set's whole premise is documented, sourced history, so an invented quote + attributed to a real person is the worst defect it can have. `enhance` is + therefore opt-in only and never part of `all`. Running it means accepting that + every affected face must be checked by eye before publish. + + PORTRAITS ARE CONDITIONED ON A REAL PHOTOGRAPH + ---------------------------------------------- + The first pass generated faces from text prompts alone, so every portrait was an + invented stranger. Generation now takes a verified reference photo as input (see + scripts/women-refs.mjs), and a card with no verified photo gets NO FACE — the + front renders a plate saying so. We do not invent a likeness of a real person. + + The reference photo's photographer is credited: most are CC BY / CC BY-SA, and a + conditioned portrait is a derivative work. data/women-references.json carries + the provenance and the card page prints it. + + The roster is src/data/women.ts, imported directly (Node strips the types), + so there is no second copy of the data to keep in sync. + + Portrait resolution order, highest authority first: + 1. assets/portraits/women-in-tech/card_NNN.png (human-approved, committed) + 2. assets/portraits-art/women-in-tech/card_NNN.png (model output, gitignored) + 3. generate a new one + + Env: + OPENAI_API_KEY / GEMINI_API_KEY at least one, picks the provider + IMAGE_PROVIDER=openai|gemini override auto-detection + OPENAI_IMAGE_MODEL default gpt-image-2 + GEMINI_IMAGE_MODEL default gemini-3-pro-image-preview + CHROME_PATH override browser discovery + + Run: + node scripts/women-cards.mjs all # whole set + node scripts/women-cards.mjs all 1 5 12 # only these numbers + node scripts/women-cards.mjs render # no API calls, no spend +*/ +import fs from 'node:fs/promises'; +import fssync from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; +import { chromium } from 'playwright-core'; +import sharp from 'sharp'; +import { buildFront, buildBack, CARD_W, CARD_H } from './women-template.mjs'; + +// What the site serves. Series One publishes at exactly this size and all three +// decks appear in the same grids, so Series Three has to match. +const WEB_W = 500; +const WEB_H = 745; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +// Importing src/data/women.ts makes Node warn that package.json has no "type". +// Setting it would change module resolution for the Next app, so mute just this +// one warning rather than 30 lines of noise on every run. +const emitWarning = process.emitWarning.bind(process); +process.emitWarning = (warning, ...rest) => { + const code = rest.find((r) => typeof r === 'string' && r.startsWith('MODULE_')) ?? + rest.find((r) => r && typeof r === 'object')?.code; + if (code === 'MODULE_TYPELESS_PACKAGE_JSON') return; + return emitWarning(warning, ...rest); +}; + +const REFS_DIR = path.join(ROOT, 'assets', 'references', 'women-in-tech'); +const REF_MANIFEST = path.join(ROOT, 'data', 'women-references.json'); + +const DIRS = { + approvedArt: path.join(ROOT, 'assets', 'portraits', 'women-in-tech'), + workingArt: path.join(ROOT, 'assets', 'portraits-art', 'women-in-tech'), + html: path.join(ROOT, 'dist', 'women', 'html'), + enhanced: path.join(ROOT, 'dist', 'women', 'enhanced'), + published: path.join(ROOT, 'public', 'cards', 'women'), +}; + +const CONCURRENCY = 3; +const cardId = (n) => `card_${String(n).padStart(3, '0')}`; + +// ---------------------------------------------------------------- roster ---- + +async function loadRoster(only = []) { + const mod = await import(path.join(ROOT, 'src', 'data', 'women.ts')); + const all = mod.builders; + if (!only.length) return all; + const found = all.filter((h) => only.includes(h.number)); + const missing = only.filter((n) => !all.some((h) => h.number === n)); + if (missing.length) throw new Error(`no such card number: ${missing.join(', ')}`); + return found; +} + +// ------------------------------------------------------------- providers ---- + +function provider() { + const forced = process.env.IMAGE_PROVIDER; + if (forced) { + if (!['openai', 'gemini'].includes(forced)) throw new Error(`unknown IMAGE_PROVIDER: ${forced}`); + return forced; + } + if (process.env.OPENAI_API_KEY) return 'openai'; + if (process.env.GEMINI_API_KEY) return 'gemini'; + throw new Error('set OPENAI_API_KEY or GEMINI_API_KEY to generate art'); +} + +const OPENAI_MODEL = () => process.env.OPENAI_IMAGE_MODEL || 'gpt-image-2'; +const GEMINI_MODEL = () => process.env.GEMINI_IMAGE_MODEL || 'gemini-3-pro-image-preview'; + +const RETRYABLE = /quota|rate|429|500|502|503|timeout|unavailable|internal|overloaded/i; + +async function withRetry(label, fn, attempts = 3) { + let lastErr; + for (let attempt = 1; attempt <= attempts; attempt++) { + try { + return await fn(); + } catch (err) { + lastErr = err; + if (attempt === attempts || !RETRYABLE.test(String(err.message))) break; + const wait = 4000 * attempt; + process.stdout.write(` … ${label} retry ${attempt}/${attempts - 1} in ${wait / 1000}s\n`); + await new Promise((r) => setTimeout(r, wait)); + } + } + throw lastErr; +} + +/** Text prompt -> PNG buffer. */ +async function generateImage(prompt, { size = '1024x1024' } = {}) { + if (provider() === 'gemini') { + const res = await fetch( + `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL()}:generateContent?key=${process.env.GEMINI_API_KEY}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contents: [{ parts: [{ text: prompt }] }], + generationConfig: { responseModalities: ['IMAGE'] }, + }), + } + ); + const d = await res.json(); + if (!d.candidates) throw new Error(JSON.stringify(d.error || d).slice(0, 200)); + const part = d.candidates[0].content.parts.find((p) => p.inlineData || p.inline_data); + if (!part) throw new Error('response contained no image'); + return Buffer.from((part.inlineData || part.inline_data).data, 'base64'); + } + + const res = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ model: OPENAI_MODEL(), prompt, size, n: 1 }), + }); + const d = await res.json(); + if (d.error) throw new Error(d.error.message?.slice(0, 200) || 'openai error'); + if (!d.data?.[0]?.b64_json) throw new Error('response contained no image'); + return Buffer.from(d.data[0].b64_json, 'base64'); +} + +/** Source PNG + instruction -> revised PNG buffer. */ +async function editImage(pngBuffer, prompt, { size = '1024x1536' } = {}) { + if (provider() === 'gemini') { + const res = await fetch( + `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL()}:generateContent?key=${process.env.GEMINI_API_KEY}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contents: [ + { + parts: [ + { inline_data: { mime_type: 'image/png', data: pngBuffer.toString('base64') } }, + { text: prompt }, + ], + }, + ], + generationConfig: { responseModalities: ['IMAGE'] }, + }), + } + ); + const d = await res.json(); + if (!d.candidates) throw new Error(JSON.stringify(d.error || d).slice(0, 200)); + const part = d.candidates[0].content.parts.find((p) => p.inlineData || p.inline_data); + if (!part) throw new Error('response contained no image'); + return Buffer.from((part.inlineData || part.inline_data).data, 'base64'); + } + + const form = new FormData(); + form.append('model', OPENAI_MODEL()); + form.append('prompt', prompt); + form.append('size', size); + form.append('image', new Blob([pngBuffer], { type: 'image/png' }), 'card.png'); + const res = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` }, + body: form, + }); + const d = await res.json(); + if (d.error) throw new Error(d.error.message?.slice(0, 200) || 'openai error'); + if (!d.data?.[0]?.b64_json) throw new Error('response contained no image'); + return Buffer.from(d.data[0].b64_json, 'base64'); +} + +// -------------------------------------------------------------- prompts ---- + +function referenceFor(h) { + const p = path.join(REFS_DIR, `${cardId(h.number)}.jpg`); + return fssync.existsSync(p) ? p : null; +} + +let refManifest = null; +function refInfo(slug) { + if (!refManifest) { + refManifest = fssync.existsSync(REF_MANIFEST) + ? JSON.parse(fssync.readFileSync(REF_MANIFEST, 'utf8')) + : []; + } + return refManifest.find((r) => r.slug === slug)?.ref || null; +} + +/** + * Image-to-image instruction. The reference photograph is the subject; the model's + * job is to restyle it, not to reimagine who it is. Says so several ways, because + * the failure we are guarding against is precisely a plausible different face. + */ +function likenessPrompt(h) { + const motif = (h.domains || []).slice(0, 2).join(' and ') || 'computing'; + return [ + 'Repaint this photograph as a realistic semi-painterly portrait for a premium collectible trading card.', + 'CRITICAL: keep the SAME PERSON. Preserve the exact facial structure, likeness, age, hairstyle, facial hair,', + 'glasses, skin tone and expression of the person in the photograph. Do not beautify, do not de-age,', + 'do not substitute a different face. The result must be recognisable as the same individual.', + 'Head and shoulders, facing the viewer, warm cinematic studio lighting with a cool rim light,', + 'detailed painted realism, dignified and serious.', + `Replace the background with a dark background carrying subtle faint motifs of ${motif}.`, + 'No text, no letters, no logos, no border, no card frame, no watermark.', + ].join(' '); +} + +function portraitPrompt(h) { + const motif = (h.domains || []).slice(0, 2).join(' and ') || 'computing'; + const era = h.era ? ` Period-accurate to ${h.era}.` : ''; + return [ + `Realistic semi-painterly digital portrait of ${h.name}, ${h.title}.`, + 'Head and shoulders, facing the camera, warm cinematic studio lighting with a cool rim light,', + 'detailed painted realism in the style of a premium collectible trading-card portrait.', + `Dark background with subtle faint motifs of ${motif}.${era}`, + 'Recognizable likeness, dignified and serious, treated as a documented historical figure.', + 'No text, no letters, no logos, no border, no card frame, no watermark.', + ].join(' '); +} + +const ENHANCE_PROMPT = + 'Enhance this collectible trading card into a premium, professionally-printed card with real depth and polish: ' + + 'cinematic lighting and painterly refinement on the portrait, refined dark metal and matte material textures, ' + + 'a tasteful metallic foil accent border, embossed elements, soft inner shadows and subtle gloss — like a high-end ' + + 'collectible trading card. ABSOLUTELY CRITICAL: keep ALL text, numbers, names, aliases, titles, labels, stat values, ' + + 'bar lengths, the quote and the entire layout EXACTLY the same, fully legible and in identical positions — do not ' + + 'change, add, remove, reflow or misspell any text. Only improve the visual finish, depth, materials and lighting.'; + +// -------------------------------------------------------------- browser ---- + +function findChrome() { + if (process.env.CHROME_PATH) return process.env.CHROME_PATH; + const candidates = [ + '/usr/bin/google-chrome', + '/usr/bin/chromium', + '/usr/bin/chromium-browser', + ...playwrightChromiums(), + ]; + const found = candidates.find((p) => p && fssync.existsSync(p)); + if (!found) { + throw new Error( + 'no Chrome/Chromium found. Set CHROME_PATH, or install one with `npx playwright install chromium`.' + ); + } + return found; +} + +/** + * A Playwright Chromium unpacked without root has no system GTK/ATK libraries. + * If a staged copy is present, put it on the loader path so the browser starts. + * Absent that directory this is a no-op and the system libraries are used. + */ +function browserEnv() { + const staged = path.join( + process.env.HOME || '', + '.local', + 'share', + 'chrome-deps', + 'usr', + 'lib', + 'x86_64-linux-gnu' + ); + if (!fssync.existsSync(staged)) return undefined; + const existing = process.env.LD_LIBRARY_PATH; + return { ...process.env, LD_LIBRARY_PATH: existing ? `${staged}:${existing}` : staged }; +} + +function playwrightChromiums() { + const base = path.join(process.env.HOME || '', '.cache', 'ms-playwright'); + if (!fssync.existsSync(base)) return []; + return fssync + .readdirSync(base) + .filter((d) => d.startsWith('chromium')) + .sort() + .reverse() + .map((d) => path.join(base, d, 'chrome-linux', 'chrome')); +} + +// --------------------------------------------------------------- stages ---- + +async function ensureDirs() { + for (const d of Object.values(DIRS)) await fs.mkdir(d, { recursive: true }); +} + +function approvedPortrait(h) { + const p = path.join(DIRS.approvedArt, `${cardId(h.number)}.png`); + return fssync.existsSync(p) ? p : null; +} +function workingPortrait(h) { + const p = path.join(DIRS.workingArt, `${cardId(h.number)}.png`); + return fssync.existsSync(p) ? p : null; +} +function portraitFor(h) { + return approvedPortrait(h) || workingPortrait(h); +} + +/** Run `task` over `items` with a small worker pool, reporting as it goes. */ +async function pool(items, label, task) { + let i = 0; + let ok = 0; + let skipped = 0; + const failed = []; + async function worker() { + while (i < items.length) { + const item = items[i++]; + const name = `${String(item.number).padStart(2, '0')} ${item.name}`; + try { + const r = await task(item); + if (r === 'skip') { + skipped++; + } else { + ok++; + process.stdout.write(`✓ ${name}\n`); + } + } catch (err) { + failed.push(item.number); + process.stdout.write(`✗ ${name}: ${err.message}\n`); + } + } + } + await Promise.all(Array.from({ length: Math.min(CONCURRENCY, items.length) }, worker)); + console.log( + `${label}: ${ok} done, ${skipped} already present, ${failed.length} failed` + + (failed.length ? ` (${failed.join(', ')})` : '') + ); + return { ok, skipped, failed }; +} + +async function portraits(roster) { + await ensureDirs(); + console.log(`portraits: ${provider()} / ${provider() === 'openai' ? OPENAI_MODEL() : GEMINI_MODEL()}`); + const withRef = roster.filter((h) => referenceFor(h)); + const without = roster.filter((h) => !referenceFor(h)); + if (without.length) { + console.log( + `portraits: ${without.length} have no verified reference photo and will get no face ` + + `(${without.map((h) => h.number).join(', ')}). Run scripts/women-refs.mjs to look again.` + ); + } + return pool(withRef, 'portraits', async (h) => { + if (portraitFor(h)) return 'skip'; + // Image-to-image off the real photograph. Never generateImage() — a text-only + // prompt is what produced the invented faces this pipeline exists to prevent. + const ref = await fs.readFile(referenceFor(h)); + const buf = await withRetry(cardId(h.number), () => editImage(ref, likenessPrompt(h), { size: '1024x1024' })); + await fs.writeFile(path.join(DIRS.workingArt, `${cardId(h.number)}.png`), buf); + }); +} + +async function render(roster) { + await ensureDirs(); + const executablePath = findChrome(); + console.log(`render: ${executablePath}`); + const browser = await chromium.launch({ + executablePath, + args: ['--no-sandbox'], + env: browserEnv(), + }); + const page = await browser.newPage({ + viewport: { width: CARD_W, height: CARD_H }, + deviceScaleFactor: 2, + }); + + const shoot = async (html, id, side) => { + // Keep the self-contained HTML next to the PNG so a card can be re-rendered + // or hand-corrected later without regenerating the portrait. + await fs.writeFile(path.join(DIRS.html, `${id}-${side}.html`), html); + await page.setContent(html, { waitUntil: 'networkidle' }); + await page.evaluate(() => document.fonts.ready); + await page.locator('.card').screenshot({ path: path.join(DIRS.html, `${id}-${side}.png`) }); + }; + + let ok = 0; + const failed = []; + for (const h of roster) { + const id = cardId(h.number); + try { + const art = portraitFor(h); + // No art is a legitimate outcome: nobody with no verified reference photo + // gets an invented face. buildFront renders a "no likeness" plate instead. + const uri = art ? `data:image/png;base64,${(await fs.readFile(art)).toString('base64')}` : null; + await shoot(buildFront(h, uri, refInfo(h.slug)), id, 'front'); + await shoot(buildBack(h, refInfo(h.slug)), id, 'back'); + ok++; + console.log(`✓ ${String(h.number).padStart(2, '0')} ${h.name} (${h.rarity})`); + } catch (err) { + failed.push(h.number); + console.log(`✗ ${String(h.number).padStart(2, '0')} ${h.name}: ${err.message}`); + } + } + await browser.close(); + console.log(`render: ${ok} done, ${failed.length} failed${failed.length ? ` (${failed.join(', ')})` : ''}`); + return { ok, failed }; +} + +async function enhance(roster) { + // See the header comment. This pass rewrote text on shipped Series Two faces. + if (!process.env.ENHANCE_I_WILL_CHECK_EVERY_FACE) { + console.error( + 'enhance: refusing to run.\n' + + ' On Series Two this pass fabricated quotes attributed to living people,\n' + + ' invented stat panels and misspelled a name, despite the prompt forbidding it.\n' + + ' There is no OCR check in this pipeline, so nothing catches it but your eyes.\n' + + ' Set ENHANCE_I_WILL_CHECK_EVERY_FACE=1 to run it anyway, then inspect every\n' + + ' face in dist/women/enhanced/ before publish.' + ); + process.exit(1); + } + await ensureDirs(); + console.log(`enhance: ${provider()} / ${provider() === 'openai' ? OPENAI_MODEL() : GEMINI_MODEL()}`); + console.log('enhance: WARNING — this pass rewrites text. Check every face before publish.'); + const jobs = []; + for (const h of roster) { + for (const side of ['front', 'back']) { + const src = path.join(DIRS.html, `${cardId(h.number)}-${side}.png`); + if (fssync.existsSync(src)) jobs.push({ ...h, side, src, name: `${h.name} ${side}` }); + } + } + if (!jobs.length) { + console.log('enhance: nothing rendered yet — run `render` first'); + return { ok: 0, failed: [] }; + } + return pool(jobs, 'enhance', async (job) => { + const out = path.join(DIRS.enhanced, `${cardId(job.number)}-${job.side}.png`); + if (fssync.existsSync(out)) return 'skip'; + const buf = await withRetry( + `${cardId(job.number)}-${job.side}`, + async () => editImage(await fs.readFile(job.src), ENHANCE_PROMPT) + ); + await fs.writeFile(out, buf); + }); +} + +async function publish(roster) { + await ensureDirs(); + let ok = 0; + const missing = []; + const linked = []; + let fromHtml = 0; + for (const h of roster) { + let faces = 0; + for (const side of ['front', 'back']) { + const id = cardId(h.number); + const enhanced = path.join(DIRS.enhanced, `${id}-${side}.png`); + const plain = path.join(DIRS.html, `${id}-${side}.png`); + const src = fssync.existsSync(enhanced) ? enhanced : fssync.existsSync(plain) ? plain : null; + if (!src) { + missing.push(`${id}-${side}`); + continue; + } + if (src === plain) fromHtml++; + const dest = path.join( + DIRS.published, + `${String(h.number).padStart(3, '0')}-${h.slug}-${side}.png` + ); + // The full-resolution face stays in dist/ as the print master; the copy the + // site loads is resized and recompressed, or a deck costs ~180MB to load. + await sharp(src) + .resize(WEB_W, WEB_H, { fit: 'fill' }) + .png({ compressionLevel: 9, palette: true }) + .toFile(dest); + ok++; + faces++; + } + // Only advertise a card on the site once both of its faces exist. + if (faces === 2) linked.push(h); + } + await writeArtPaths(linked); + console.log(`publish: ${ok} faces -> ${path.relative(ROOT, DIRS.published)}`); + if (fromHtml) console.log(`publish: ${fromHtml} used the text-accurate HTML render (the expected path for this set)`); + if (missing.length) console.log(`publish: ${missing.length} missing (${missing.join(', ')})`); + return { ok, missing }; +} + +/** + * Point the roster's `front`/`back` fields at the faces we just published, so the + * site and its "artwork rendered" counter pick them up. Rewrites only those two + * lines per card and leaves the hand-written copy untouched. + */ +async function writeArtPaths(published) { + if (!published.length) return; + const file = path.join(ROOT, 'src', 'data', 'women.ts'); + const lines = (await fs.readFile(file, 'utf8')).split('\n'); + const wanted = new Map(published.map((h) => [h.number, h])); + const out = []; + const inserted = new Set(); + let current = null; + let changed = 0; + + for (const line of lines) { + const num = line.match(/^ {4}number: (\d+),$/); + if (num) current = Number(num[1]); + const rewriting = current !== null && wanted.has(current); + // Drop any previous art lines for a card we are about to rewrite. `current` + // has to stay set past the insert below, or existing lines sitting *after* + // `status:` slip through and the card ends up with duplicate keys. + if (rewriting && /^ {4}(front|back): '.*',$/.test(line)) continue; + out.push(line); + if (rewriting && !inserted.has(current) && /^ {4}status: '.*',$/.test(line)) { + const h = wanted.get(current); + const stem = `/cards/women/${String(h.number).padStart(3, '0')}-${h.slug}`; + out.push(` front: '${stem}-front.png',`); + out.push(` back: '${stem}-back.png',`); + changed++; + inserted.add(current); + } + } + + await fs.writeFile(file, out.join('\n')); + console.log(`publish: linked art paths for ${changed} card(s) in src/data/women.ts`); +} + +async function validate(roster) { + const all = await loadRoster(); + const errors = []; + const warnings = []; + + const numbers = new Set(); + const slugs = new Set(); + for (const h of all) { + if (numbers.has(h.number)) errors.push(`duplicate card number ${h.number}`); + if (slugs.has(h.slug)) errors.push(`duplicate slug ${h.slug}`); + numbers.add(h.number); + slugs.add(h.slug); + } + + for (const h of roster) { + const id = `${cardId(h.number)} ${h.name}`; + for (const field of ['slug', 'name', 'title', 'knownFor', 'rarity', 'nationality', 'era', 'scouting', 'note', 'status']) { + if (!h[field]) errors.push(`${id}: missing ${field}`); + } + if (!h.domains?.length) errors.push(`${id}: needs at least one domain`); + // The set claims to be documented history, so an unsourced card is a defect, + // not a warning. Series Two holds the same bar by hand; here it is enforced. + if (!h.sources?.length) errors.push(`${id}: needs at least one source`); + for (const s of h.sources || []) { + if (!s.label || !/^https?:\/\//.test(String(s.url))) { + errors.push(`${id}: source needs a label and an http(s) url, got ${JSON.stringify(s)}`); + } + } + if (h.domains?.length > 5) errors.push(`${id}: ${h.domains.length} domains, the back panel fits 5`); + for (const stat of ['impact', 'technical', 'pioneering', 'openness', 'influence']) { + const v = h[stat]; + if (typeof v !== 'number' || v < 0 || v > 100) errors.push(`${id}: ${stat} must be 0-100, got ${v}`); + } + const scoutWords = String(h.scouting).trim().split(/\s+/).length; + const noteWords = String(h.note).trim().split(/\s+/).length; + if (scoutWords > 70) warnings.push(`${id}: scouting report is ${scoutWords} words, panel is sized for ~65`); + if (noteWords > 35) warnings.push(`${id}: note is ${noteWords} words, sized for ~35`); + if (h.status !== 'locked') warnings.push(`${id}: status is "${h.status}", copy is not final`); + if (!portraitFor(h)) warnings.push(`${id}: no portrait art yet`); + } + + for (const w of warnings) console.log(`warn ${w}`); + if (errors.length) { + console.error('\nValidation failed:'); + for (const e of errors) console.error(` - ${e}`); + process.exit(1); + } + console.log( + `\nValidation passed: ${all.length} on the roster, ${roster.length} selected, ` + + `${warnings.length} warning${warnings.length === 1 ? '' : 's'}.` + ); + return { warnings }; +} + +// ------------------------------------------------------------------ cli ---- + +const HELP = `Security Professionals (Series Three) card pipeline + + node scripts/women-cards.mjs [card numbers...] + +Commands + validate Check the roster and report what art is still missing + portraits Generate missing portrait art + render Render card fronts and backs from the HTML template (no API calls) + enhance Premium finish pass. OPT-IN ONLY — it rewrites text. See the header. + publish Copy the best available faces into public/cards/women/ + all portraits -> render -> publish + +With no card numbers every card on the roster is processed. Every stage skips +work that already exists, so re-running only fills gaps.`; + +async function main() { + const cmd = process.argv[2] || 'help'; + const only = process.argv.slice(3).map(Number).filter((n) => Number.isFinite(n) && n > 0); + + if (cmd === 'help' || cmd === '--help' || cmd === '-h') { + console.log(HELP); + return; + } + + const roster = await loadRoster(only); + if (only.length) console.log(`Selected ${roster.length} card(s): ${roster.map((h) => h.number).join(', ')}\n`); + + if (cmd === 'validate') await validate(roster); + else if (cmd === 'portraits') await portraits(roster); + else if (cmd === 'render') await render(roster); + else if (cmd === 'enhance') await enhance(roster); + else if (cmd === 'publish') await publish(roster); + else if (cmd === 'all') { + // No enhance. See the header comment — it is opt-in for this set. + await portraits(roster); + await render(roster); + await publish(roster); + } else { + console.error(`Unknown command: ${cmd}\n`); + console.log(HELP); + process.exit(1); + } +} + +main().catch((err) => { + console.error('ERROR:', err.message); + process.exit(1); +}); diff --git a/scripts/women-refs.mjs b/scripts/women-refs.mjs new file mode 100644 index 0000000..3227763 --- /dev/null +++ b/scripts/women-refs.mjs @@ -0,0 +1,358 @@ +#!/usr/bin/env node +/* + Women in Tech (Series Five) — reference photo resolution. + + WHY THIS EXISTS + --------------- + The first art pass generated portraits from text prompts alone, so every face + was an invented stranger rather than the real person. Portraits are now + conditioned on an actual photograph, and this stage is what finds one. + + IDENTITY IS NOT ASSUMED. A Wikidata search for a name returns humans, not the + right human: "Michael Howard" resolves to the British Conservative leader, + "Jeff Williams" to a Canadian rugby player, "Ivan Ristic" to an art historian. + Shipping those would put a stranger's face on the card, which is worse than the + generic one it replaces. So a candidate is only accepted when it clears + `looksRight()` below, and everything else is reported for a human to judge. + + Resolution order, highest authority first: + 1. OVERRIDES hand-verified, including photos supplied directly + 2. Wikidata P18 free file already wired to a verified item + 3. Commons search free file that exists but is not wired to the item + 4. nothing the card gets no face; it does NOT get an invented one + + Provenance for every accepted photo is written to data/women-references.json + and is what the card page credits. A CC BY / CC BY-SA photo obliges us to name + the photographer, and a derivative portrait inherits that obligation. + + Run: + node scripts/women-refs.mjs resolve # find candidates, write the manifest + node scripts/women-refs.mjs fetch # download accepted references + node scripts/women-refs.mjs report # who is still missing a face +*/ +import fs from 'node:fs/promises'; +import fssync from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const UA = 'opensourcelegends.com card pipeline (anthony@profullstack.com)'; + +const emitWarning = process.emitWarning.bind(process); +process.emitWarning = (warning, ...rest) => { + const code = + rest.find((r) => typeof r === 'string' && r.startsWith('MODULE_')) ?? + rest.find((r) => r && typeof r === 'object')?.code; + if (code === 'MODULE_TYPELESS_PACKAGE_JSON') return; + return emitWarning(warning, ...rest); +}; + +const REFS_DIR = path.join(ROOT, 'assets', 'references', 'women-in-tech'); +const MANIFEST = path.join(ROOT, 'data', 'women-references.json'); +const cardId = (n) => `card_${String(n).padStart(3, '0')}`; + +/** + * Hand-verified references. An entry here overrides every lookup. + * + * `license` must describe the actual terms. "supplied" means the site owner + * provided the file directly and is asserting the right to use it: that is not + * a free licence, so the card credits the source rather than claiming CC terms + * the deck cannot back. + */ +const OVERRIDES = { + // Profile photographs from their own accounts, each verified against the real + // name the API reports. Not freely licensed, so recorded as 'supplied'. + // Commons search returned a French astronaut of the same name in a flight suit. + 'camille-fournier': { + url: 'https://avatars.githubusercontent.com/u/607890?v=4&s=460', + credit: 'Camille Fournier', + license: 'supplied', + sourceUrl: 'https://github.com/skamille', + note: 'Profile photograph; account verified against their real name.', + }, + 'carol-willing': { + url: 'https://avatars.githubusercontent.com/u/2680980?v=4&s=460', + credit: 'Carol Willing', + license: 'supplied', + sourceUrl: 'https://github.com/willingc', + note: 'Profile photograph; account verified against their real name.', + }, + 'sarah-drasner': { + url: 'https://avatars.githubusercontent.com/u/2281088?v=4&s=460', + credit: 'Sarah Drasner', + license: 'supplied', + sourceUrl: 'https://github.com/sdras', + note: 'Profile photograph; account verified against their real name.', + }, + 'jessie-frazelle': { + url: 'https://avatars.githubusercontent.com/u/1445228?v=4&s=460', + credit: 'Jess Frazelle', + license: 'supplied', + sourceUrl: 'https://github.com/jessfraz', + note: 'Profile photograph; account verified against their real name.', + }, + 'leonie-watson': { + url: 'https://avatars.githubusercontent.com/u/3501033?v=4&s=460', + credit: 'Leonie Watson', + license: 'supplied', + sourceUrl: 'https://github.com/LJWatson', + note: 'Profile photograph; account verified against their real name.', + }, +}; + +/** Wikidata items confirmed to be the wrong human — never accept these. */ +const REJECT_QIDS = new Set([]); + +/** + * Auto-resolution found something, a human looked at it, and it did not hold up. + * These get no face. Recorded with the reason so nobody re-adds them by rerunning + * the resolver and trusting the output. + */ +const REJECTED = { + // Looked at, did not hold up. Reasons recorded so a rerun cannot undo the call. + 'jean-bartik': + 'Wikidata P18 is "Two women operating ENIAC", a wide room shot where no face is legible. Conditioning on it would invent one.', + 'betty-holberton': + 'The only Commons photograph is a small, low-resolution profile at the ENIAC panels. Not enough face to paint a likeness from without guessing.', + 'joan-clarke': + 'Commons search returned a photograph of the house at 193 Rosendale Road that carries her blue plaque. Her Bletchley-era portraits are not freely licensed.', + 'anita-borg': + 'Commons search returned a screenshot of her Wikidata item structure, a diagram.', + 'leonie-watson': + 'Her GitHub avatar is the tink logo and her about page serves no photograph.', + 'julia-evans': + 'Commons search returned "Julia Evans Reed.jpg", a different person, and her GitHub avatar is a cartoon star rather than a photograph.', + 'kathleen-booth': + 'No freely licensed photograph found. Wikipedia carries no portrait.', +}; + +const j = async (url) => { + const r = await fetch(url, { headers: { 'User-Agent': UA } }); + if (!r.ok) throw new Error(`${r.status} for ${url}`); + return r.json(); +}; + +async function loadRoster() { + // src/data/women.ts statically imports the manifest, so it has to exist + // before the roster can be loaded — including on the very first resolve, and + // after someone deletes it to force a clean run. + if (!fssync.existsSync(MANIFEST)) { + await fs.mkdir(path.dirname(MANIFEST), { recursive: true }); + await fs.writeFile(MANIFEST, '[]\n'); + } + const mod = await import(path.join(ROOT, 'src', 'data', 'women.ts')); + return mod.builders; +} + +/** + * Does this Wikidata item plausibly describe the person on the card? + * Deliberately conservative: a miss costs a manual lookup, a false accept ships + * a stranger's face. + */ +const FIELD = + /comput|software|engineer|scientist|research|professor|technolog|inform|program|mathematic|cryptanal|cryptograph|physicist|invent|designer|activist|entrepreneur|business|academic|actress|writer|analyst/i; + +function looksRight(entity, pro) { + if (!entity) return false; + const claims = entity.claims || {}; + if (!(claims.P31 || []).some((c) => c.mainsnak?.datavalue?.value?.id === 'Q5')) return false; + + const label = entity.labels?.en?.value || ''; + const desc = entity.descriptions?.en?.value || ''; + const aliases = (entity.aliases?.en || []).map((a) => a.value); + + // Surname must actually appear — "Mark Dowd" matching "Mark Dowdall" is a miss. + const surname = pro.name.split(/\s+/).pop().toLowerCase(); + const names = [label, ...aliases].map((s) => s.toLowerCase()); + if (!names.some((n) => n.split(/\s+/).includes(surname))) return false; + + // No birth-year floor either, for the same reason. Ada Lovelace was born in 1815. + + return FIELD.test(desc) || FIELD.test(label); +} + + +const norm = (s) => + String(s) + .toLowerCase() + .replace(/[._-]+/g, ' ') + .replace(/[^a-z0-9 ]+/g, '') + .replace(/\s+/g, ' ') + .trim(); + +/** + * True only when the filename *begins* with the person's full name, so + * "Alex Sotirov.jpg" and "J. Alex Halderman - 2018.jpg" pass while + * "Martin Wagenleiter (Roesch) 861x1200.jpg" and "HT-Jeffrey-Williams.jpg" do not. + */ +function filenameOpensWithName(filename, name) { + const f = norm(filename.replace(/\.[a-z0-9]+$/i, '')); + return f.startsWith(norm(name)); +} + +/** Commons file -> direct URL plus the licence and author we are obliged to credit. */ +async function commonsMeta(filename) { + const d = await j( + `https://commons.wikimedia.org/w/api.php?action=query&format=json&prop=imageinfo&iiprop=url|extmetadata&iiurlwidth=1024&titles=${encodeURIComponent('File:' + filename)}` + ); + const page = Object.values(d.query?.pages || {})[0]; + const info = page?.imageinfo?.[0]; + if (!info) return null; + const meta = info.extmetadata || {}; + const strip = (v) => String(v?.value ?? '').replace(/<[^>]*>/g, '').trim(); + // NO DATE GUARD IN THIS SET. Series Three rejected pre-1950 photographs because + // an old plate meant the search had found a historical namesake. Here the + // historical figures ARE the subject: Lovelace died in 1852, the ENIAC women were + // photographed in 1946, and Joan Clarke's Bletchley portraits are wartime. That + // guard would have silently deleted the first third of the roster. + + return { + url: info.thumburl || info.url, + credit: strip(meta.Artist) || 'Unknown', + license: strip(meta.LicenseShortName) || 'unknown', + licenseUrl: strip(meta.LicenseUrl) || '', + sourceUrl: info.descriptionurl, + file: filename, + }; +} + +async function resolve() { + const roster = await loadRoster(); + const manifest = []; + + for (const pro of roster) { + const row = { number: pro.number, slug: pro.slug, name: pro.name, ref: null, candidates: [] }; + + if (REJECTED[pro.slug]) { + row.rejected = REJECTED[pro.slug]; + manifest.push(row); + console.log(`${String(pro.number).padStart(2)} ${pro.name.padEnd(22)} REJECTED ${row.rejected.slice(0, 58)}`); + continue; + } + + if (OVERRIDES[pro.slug]) { + const o = OVERRIDES[pro.slug]; + row.ref = o.commonsFile + ? { ...(await commonsMeta(o.commonsFile)), ...o, via: 'override' } + : { ...o, via: 'override' }; + manifest.push(row); + console.log(`${String(pro.number).padStart(2)} ${pro.name.padEnd(22)} OVERRIDE ${row.ref.license}`); + continue; + } + + let entity = null; + let qid = null; + try { + const s = await j( + `https://www.wikidata.org/w/api.php?action=wbsearchentities&search=${encodeURIComponent(pro.name)}&language=en&format=json&limit=6&type=item` + ); + for (const hit of s.search || []) { + if (REJECT_QIDS.has(hit.id)) continue; + const e = await j(`https://www.wikidata.org/wiki/Special:EntityData/${hit.id}.json`); + const ent = e.entities?.[hit.id]; + if (looksRight(ent, pro)) { + entity = ent; + qid = hit.id; + break; + } + } + } catch (err) { + row.error = String(err.message).slice(0, 80); + } + + const p18 = entity?.claims?.P18?.[0]?.mainsnak?.datavalue?.value; + if (p18) { + const meta = await commonsMeta(p18); + if (meta) { + row.ref = { ...meta, qid, via: 'wikidata-p18' }; + row.desc = entity.descriptions?.en?.value || ''; + } + } + + // No P18: a free file may still exist on Commons, unlinked. Containing the + // name somewhere is far too weak a test — "Martin Wagenleiter (Roesch)" and + // "Merseyside PTE 40th anniversary" both matched that way, and a search for + // "Robert M. Lee" happily returns a daguerreotype of the Confederate general. + // So the filename must OPEN with the person's full name. + if (!row.ref) { + try { + const s = await j( + `https://commons.wikimedia.org/w/api.php?action=query&format=json&generator=search&gsrnamespace=6&gsrlimit=10&gsrsearch=${encodeURIComponent(pro.name)}` + ); + for (const p of Object.values(s.query?.pages || {})) { + const f = p.title.replace(/^File:/, ''); + if (!/\.(jpe?g|png)$/i.test(f)) continue; + if (!filenameOpensWithName(f, pro.name)) continue; + row.candidates.push(f); + } + if (row.candidates.length) { + const meta = await commonsMeta(row.candidates[0]); + if (meta) row.ref = { ...meta, qid, via: 'commons-search' }; + } + } catch {} + } + + manifest.push(row); + const tag = row.ref ? row.ref.via.toUpperCase().padEnd(10) : 'NONE '; + console.log( + `${String(pro.number).padStart(2)} ${pro.name.padEnd(22)} ${tag} ${row.ref ? `${row.ref.license} | ${row.ref.credit.slice(0, 34)}` : (row.desc || '')}` + ); + } + + await fs.mkdir(path.dirname(MANIFEST), { recursive: true }); + await fs.writeFile(MANIFEST, JSON.stringify(manifest, null, 2) + '\n'); + const got = manifest.filter((m) => m.ref).length; + console.log(`\nresolve: ${got}/${manifest.length} have a reference photo -> ${path.relative(ROOT, MANIFEST)}`); + const none = manifest.filter((m) => !m.ref); + console.log(`no photo (${none.length}) — these get no face, never an invented one:`); + for (const m of none) console.log(` ${String(m.number).padStart(2)} ${m.name}${m.rejected ? ' — ' + m.rejected.slice(0, 70) : ''}`); +} + +async function fetchRefs() { + const manifest = JSON.parse(await fs.readFile(MANIFEST, 'utf8')); + await fs.mkdir(REFS_DIR, { recursive: true }); + let ok = 0; + const failed = []; + for (const row of manifest.filter((m) => m.ref)) { + const dest = path.join(REFS_DIR, `${cardId(row.number)}.jpg`); + if (fssync.existsSync(dest)) { + ok++; + continue; + } + try { + const r = await fetch(row.ref.url, { headers: { 'User-Agent': UA } }); + if (!r.ok) throw new Error(String(r.status)); + await fs.writeFile(dest, Buffer.from(await r.arrayBuffer())); + ok++; + console.log(`✓ ${String(row.number).padStart(2)} ${row.name}`); + } catch (err) { + failed.push(`${row.number} ${row.name}: ${err.message}`); + } + } + console.log(`fetch: ${ok} references in ${path.relative(ROOT, REFS_DIR)}, ${failed.length} failed`); + for (const f of failed) console.log(' ✗ ' + f); +} + +async function report() { + const manifest = JSON.parse(await fs.readFile(MANIFEST, 'utf8')); + const byLicense = {}; + for (const m of manifest.filter((x) => x.ref)) { + byLicense[m.ref.license] = (byLicense[m.ref.license] || 0) + 1; + } + console.log('references by licence:', byLicense); + const missing = manifest.filter((m) => !m.ref); + console.log(`\n${missing.length} with no reference photo — these get no face, not an invented one:`); + for (const m of missing) console.log(` ${String(m.number).padStart(2)} ${m.name}`); +} + +const cmd = process.argv[2] || 'resolve'; +const fns = { resolve, fetch: fetchRefs, report }; +if (!fns[cmd]) { + console.error(`unknown command: ${cmd}\n\n resolve | fetch | report`); + process.exit(1); +} +fns[cmd]().catch((err) => { + console.error('ERROR:', err.message); + process.exit(1); +}); diff --git a/scripts/women-template.mjs b/scripts/women-template.mjs new file mode 100644 index 0000000..a871d36 --- /dev/null +++ b/scripts/women-template.mjs @@ -0,0 +1,210 @@ +// HTML/CSS card template for "Women in Tech" (Series Five). +// Same bones as the other four decks so the whole product line reads as one thing: +// thin accent border, portrait-top front, cream panels, dark skill stack on the back. +// What differs is the axes. This set spans 1815 to now across wildly different +// disciplines, so it rates depth, whether they went first, how much they gave away, +// and what changed after. +// Only raster image = the portrait. Everything else is HTML/CSS/SVG. +// +// Carried over from the Series Two defects: +// 1. The curator's note is a labelled panel, NOT wrapped in quote marks. Quoted, it +// reads as something the subject said, and the field is editorial voice. +// 2. No undecorated empty space on either face, because the image-to-image finish +// pass treats a blank region as an invitation to invent text. (This set does not +// run that pass at all.) + +export const CARD_W = 700; +export const CARD_H = 1043; + +// Distinct from the other four decks without being cute about it. +const PALETTE = ['c86fa8', '5b8cf0', 'd9a13a', '3fae96', '9b6bff', 'd4614f', '4f9f6a', 'b07ad6']; +export function accentHexOf(pro) { + let h = 0; + for (const ch of String(pro.name)) h = (h * 31 + ch.charCodeAt(0)) >>> 0; + return PALETTE[h % PALETTE.length]; +} +const accentOf = (pro) => '#' + accentHexOf(pro); + +// Like Series Two, every entry carries an explicit `rarity`, so unlike Series One +// it is never derived from the impact score. +export const rarityOf = (pro) => pro.rarity; +const STARS = { iconic: '★★★★★', legendary: '★★★★☆', epic: '★★★☆☆', rare: '★★☆☆☆' }; + +const pad2 = (n) => String(n).padStart(2, '0'); +const esc = (s) => String(s ?? '').replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c])); + +function traits(h) { + const m = [ + ['DEEP TECH', h.technical], + ['PIONEER', h.pioneering], + ['OPEN SOURCE', h.openness], + ['INFLUENTIAL', h.influence], + ] + .sort((a, b) => b[1] - a[1]) + .slice(0, 2) + .map((x) => x[0]); + return [...m, String(h.rarity).toUpperCase()]; +} + +function bars(h, accent) { + const rows = [ + ['TECHNICAL DEPTH', h.technical], + ['WENT FIRST', h.pioneering], + ['OPENNESS', h.openness], + ['INFLUENCE', h.influence], + ]; + // No IMPACT row: unlike Series One's "OPEN SOURCE IMPACT", it would repeat the + // score already shown in the box beside the stack. + return rows + .map( + ([label, v]) => + `
${esc(label)}${v}
` + ) + .join(''); +} + +const FONTS = `@import url('https://fonts.googleapis.com/css2?family=Oswald:wght@500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap');`; + +// Series Five mark: the same shield as the other decks, holding a three-node graph. +const emblem = (a, s = 24) => + ``; +const lockGlyph = (a, s = 22) => + ``; + +const shell = (accent) => `${FONTS} + * { margin:0; padding:0; box-sizing:border-box; } + html,body { width:${CARD_W}px; height:${CARD_H}px; } + body { font-family:'Inter',Arial,sans-serif; background:#05060a; } + .card { position:relative; width:${CARD_W}px; height:${CARD_H}px; overflow:hidden; border-radius:26px; + background: radial-gradient(120% 80% at 50% 0%, #141a18 0%, #0b0f11 60%, #08090c 100%); + box-shadow: inset 0 0 0 2px ${accent}, inset 0 0 0 5px #07070d; } + .crest { display:flex; align-items:center; gap:7px; } + .crest .t { font-family:'Oswald'; color:#fff; font-weight:700; font-size:11px; letter-spacing:.6px; line-height:1.05; }`; + +export function buildFront(pro, portraitDataUri, ref) { + const accent = accentOf(pro); + const kw = traits(pro).join('  ·  '); + const alias = pro.handle + ? `
“${esc(pro.handle)}”
` + : ''; + return ` +
+
${ + portraitDataUri + ? `` + : `${emblem(accent, 150)}${esc( + pro.name.split(/\s+/).map((w) => w[0]).join('') + )}no freely-licensed photograph
of this person was found
` + }
+
${pad2(pro.number)}
+
${emblem(accent, 24)}WOMEN
IN TECH
+
${esc(pro.name)}
${alias}
${esc(pro.title)}
+
${emblem(accent, 30)}${kw}${lockGlyph(accent, 28)}
+
`; +} + +export function buildBack(pro, ref) { + const accent = accentOf(pro); + const rarity = String(pro.rarity).toUpperCase(); + const domains = (pro.domains || []).slice(0, 5).map((d) => `
  • ${esc(d)}
  • `).join(''); + const alias = pro.handle ? `“${esc(pro.handle)}”` : ''; + return ` +
    +
    +
    ${pad2(pro.number)}${esc(pro.name)}${alias}
    +
    ${esc(pro.title)}
    +
    +
    +
    +

    SCOUTING REPORT

    ${esc(pro.scouting)}

    +

    DOMAINS

      ${domains}
    +
    +
    +

    SKILL STACK

    ${bars(pro, accent)}
    +
    IMPACT${pro.impact}${rarity}${STARS[pro.rarity] || '★★★★★'}
    +
    +
    ${emblem(accent, 190)}${ + pro.handle ? `${esc(pro.handle)}` : '' + }
    +
    CURATOR’S NOTE

    ${esc(pro.note)}

    +
    +
    ERA ${esc(pro.era)}NATIONALITY ${esc(pro.nationality)}
    +
    KNOWN FOR ${esc(pro.knownFor)}${lockGlyph(accent, 18)}
    + ${ + ref + ? `
    PORTRAIT AFTER A PHOTOGRAPH BY ${esc( + ref.credit + )}${ref.license && ref.license !== 'supplied' ? ` · ${esc(ref.license)}` : ''}
    ` + : '' + } +
    +
    `; +} diff --git a/src/app/page.tsx b/src/app/page.tsx index e8d2463..28c7cb8 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,6 +5,7 @@ import { cards, featured } from '@/data/cards'; import { hackers, illustratedCount, lockedCount } from '@/data/hacking'; import { pros, totalPlanned as proTotal } from '@/data/security'; import { architects, illustratedCount as aiIllustrated, totalPlanned as aiTotal } from '@/data/ai'; +import { builders, illustratedCount as wIllustrated, totalPlanned as wTotal } from '@/data/women'; import { site } from '@/data/site'; import styles from './page.module.css'; @@ -150,6 +151,28 @@ export default function Home() { + {/* Series Five teaser */} +
    +
    +
    +
    + Series Five · In progress +

    Women in Tech

    +

    + One hundred and eighty years of it: the first compiler, the first assembler, + the ARM instruction set, the Apollo guidance software, and the code of + conduct in your repository. {builders.length} cards, {wIllustrated} of{' '} + {wTotal} illustrated from real photographs. Series One has forty-seven cards + and no women on any of them, which is the reason this set exists. +

    +
    +
    + See the roster → +
    +
    +
    +
    + {/* In-content ad */}
    diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 13edd26..65aa33b 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -3,6 +3,7 @@ import { cards } from '@/data/cards'; import { hackers } from '@/data/hacking'; import { pros } from '@/data/security'; import { architects } from '@/data/ai'; +import { builders } from '@/data/women'; import { site } from '@/data/site'; // Small enough to stay a single sitemap — the static pages plus one per illustrated card. @@ -15,6 +16,7 @@ export default function sitemap(): MetadataRoute.Sitemap { '/hacking-legends', '/security-professionals', '/gods-of-ai', + '/women-in-tech', '/collect', '/contribute', ].map( @@ -55,5 +57,13 @@ export default function sitemap(): MetadataRoute.Sitemap { priority: 0.6, })); - return [...staticPages, ...cardPages, ...hackerPages, ...proPages, ...aiPages]; + const womenPages = builders + .filter((b) => b.front) + .map((b) => ({ + url: `${site.url}/women-in-tech/${b.slug}`, + changeFrequency: 'monthly' as const, + priority: 0.6, + })); + + return [...staticPages, ...cardPages, ...hackerPages, ...proPages, ...aiPages, ...womenPages]; } diff --git a/src/app/women-in-tech/[slug]/opengraph-image.tsx b/src/app/women-in-tech/[slug]/opengraph-image.tsx new file mode 100644 index 0000000..a72d293 --- /dev/null +++ b/src/app/women-in-tech/[slug]/opengraph-image.tsx @@ -0,0 +1,28 @@ +import { builders, getBuilder, rarityLabel } from '@/data/women'; +import { OG_CONTENT_TYPE, OG_SIZE, renderCardOg } from '@/lib/og'; + +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; +export const alt = 'Women in Tech trading card'; + +export function generateStaticParams() { + return builders.filter((p) => p.front).map((p) => ({ slug: p.slug })); +} + +export default async function Image({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + const p = getBuilder(slug); + if (!p?.front) throw new Error(`No illustrated card for slug ${slug}`); + + return renderCardOg({ + number: p.number, + name: p.name, + handle: p.handle, + title: p.title, + rarity: p.rarity, + rarityLabel: rarityLabel[p.rarity], + impact: p.impact, + front: p.front, + setLabel: 'Women in Tech', + }); +} diff --git a/src/app/women-in-tech/[slug]/page.tsx b/src/app/women-in-tech/[slug]/page.tsx new file mode 100644 index 0000000..3e2da2c --- /dev/null +++ b/src/app/women-in-tech/[slug]/page.tsx @@ -0,0 +1,105 @@ +import type { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import CardDetail from '@/components/CardDetail'; +import { builders, getBuilder, rarityLabel, statusLabel, getPortraitCredit } from '@/data/women'; +import { site } from '@/data/site'; + +type Params = { params: Promise<{ slug: string }> }; + +// Only illustrated cards get a page — an entry with no face has nothing to show, +// and listing it here would prerender 404s. +export function generateStaticParams() { + return builders.filter((p) => p.front).map((p) => ({ slug: p.slug })); +} + +export async function generateMetadata({ params }: Params): Promise { + const { slug } = await params; + const p = getBuilder(slug); + if (!p) return { title: 'Card not found' }; + + const title = `${p.name} — ${p.title}`; + const description = `${p.knownFor}. Card #${String(p.number).padStart(3, '0')} in ${site.name} Series Five: Women in Tech, ${rarityLabel[p.rarity]} rarity, impact ${p.impact}.`; + const url = `${site.url}/women-in-tech/${p.slug}`; + + return { + title, + description, + alternates: { canonical: url }, + openGraph: { title, description, url, type: 'profile' }, + twitter: { card: 'summary_large_image', title, description }, + }; +} + +export default async function BuilderPage({ params }: Params) { + const { slug } = await params; + const p = getBuilder(slug); + if (!p) notFound(); + + const i = builders.findIndex((x) => x.slug === p.slug); + const prev = i > 0 ? builders[i - 1] : undefined; + const next = i < builders.length - 1 ? builders[i + 1] : undefined; + + // A card page exists only once its art does. While the set is unillustrated this + // is every slug, which is intended — better a 404 than a page with no card on it. + if (!p.front || !p.back) notFound(); + + const jsonLd = { + '@context': 'https://schema.org', + '@type': 'Person', + name: p.name, + alternateName: p.handle, + jobTitle: p.title, + description: p.scouting, + nationality: p.nationality, + url: `${site.url}/women-in-tech/${p.slug}`, + image: `${site.url}${p.front}`, + ...(p.sources?.length ? { sameAs: p.sources.map((s) => s.url) } : {}), + subjectOf: { + '@type': 'CreativeWork', + name: `${p.name} — ${site.name} Women in Tech card #${String(p.number).padStart(3, '0')}`, + license: 'https://creativecommons.org/licenses/by-sa/4.0/', + }, + }; + + return ( + <> +