max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
6 files changed,
+645 insertions,
-192 deletions
| @@ -37,20 +37,23 @@ | |||
| 37 | 37 | MIN_PROJECTS=5 | |
| 38 | 38 | ||
| 39 | 39 | # Items a visitor can actually see and click, summed across every storefront | |
| 40 | - | # that is not paywalled. Nine of eleven on 2026-08-06; the other two are behind | |
| 41 | - | # the one deliberate subscription paywall. | |
| 42 | - | MIN_VISIBLE_ITEMS=9 | |
| 40 | + | # that is not paywalled. Twenty of twenty-five on 2026-08-07; the other five are | |
| 41 | + | # behind the one deliberate subscription paywall (the Marginalia Reader). | |
| 42 | + | # | |
| 43 | + | # Was nine of eleven until the catalog was widened to five items per project, so | |
| 44 | + | # that three-cards-in-a-five-wide-grid stopped reading as an empty shop. | |
| 45 | + | MIN_VISIBLE_ITEMS=20 | |
| 43 | 46 | ||
| 44 | 47 | # Covers still served from the seed's generated placeholder (a 16x16 grayscale | |
| 45 | - | # PNG, src/seed/media.rs). Five of five on 2026-08-06: the seed has never | |
| 46 | - | # uploaded real art, and swapping in public-domain media is its own task | |
| 47 | - | # (GoingsOn 223f3c6f, which also gates the landing captures 54730d4d). | |
| 48 | + | # PNG, src/seed/media.rs). Zero as of 2026-08-07: src/seed/media-manifest.toml | |
| 49 | + | # now names a real CC0 asset for every cover the seed attaches, so a placeholder | |
| 50 | + | # appearing at all means either the manifest stopped resolving or storage did. | |
| 48 | 51 | # | |
| 49 | - | # This is sealed rather than asserted at zero on purpose. Zero would make the | |
| 50 | - | # script red from its first run, and a check that is always red is a check | |
| 51 | - | # nobody reads. Sealed, it still catches the regression that matters: a project | |
| 52 | - | # whose real cover reverts to a placeholder pushes the count up and fails. | |
| 53 | - | PLACEHOLDER_COVERS_BASELINE=5 | |
| 52 | + | # It was sealed at five while the seed had never uploaded real art, on the | |
| 53 | + | # reasoning that a check which is always red is a check nobody reads. That | |
| 54 | + | # reason is spent: zero is now the true value, and the baseline may only move | |
| 55 | + | # in the improving direction. | |
| 56 | + | PLACEHOLDER_COVERS_BASELINE=0 | |
| 54 | 57 | ||
| 55 | 58 | # A cover at or under this is the placeholder or a broken upload, not artwork. | |
| 56 | 59 | # The placeholder is 72 bytes; real cover art is orders of magnitude larger. | |
| @@ -179,7 +182,7 @@ | |||
| 179 | 182 | if [ "$placeholders" -gt "$PLACEHOLDER_COVERS_BASELINE" ]; then | |
| 180 | 183 | fail "$placeholders placeholder covers, sealed baseline is $PLACEHOLDER_COVERS_BASELINE (a real cover reverted)" | |
| 181 | 184 | elif [ "$placeholders" -eq "$PLACEHOLDER_COVERS_BASELINE" ] && [ "$placeholders" -gt 0 ]; then | |
| 182 | - | warn "$placeholders of $n_covers covers are still the seed placeholder (GoingsOn 223f3c6f); at baseline, not a regression" | |
| 185 | + | warn "$placeholders of $n_covers covers are still the seed placeholder; at baseline, not a regression" | |
| 183 | 186 | else | |
| 184 | 187 | pass "$placeholders placeholder covers, down from a baseline of $PLACEHOLDER_COVERS_BASELINE. Lower PLACEHOLDER_COVERS_BASELINE to $placeholders" | |
| 185 | 188 | fi |
| @@ -59,7 +59,7 @@ | |||
| 59 | 59 | // point of one script: change a number here and all three move together. | |
| 60 | 60 | const VIEWPORT = { width: 1440, height: 1600 }; | |
| 61 | 61 | const SCALE = 2; // 2x for a crisp asset on retina displays | |
| 62 | - | const ASPECT = Number(process.env.CAPTURE_ASPECT ?? 16 / 10); // the carousel's frame ratio | |
| 62 | + | const ASPECT = Number(process.env.CAPTURE_ASPECT ?? 3 / 2); // the carousel's frame ratio | |
| 63 | 63 | // Every frame is cropped to exactly this many CSS pixels wide, then to ASPECT. | |
| 64 | 64 | // Deriving the width from each page's own container instead produced a 2400px | |
| 65 | 65 | // storefront next to a 1600px item page: same ratio, different effective zoom, | |
| @@ -72,13 +72,15 @@ | |||
| 72 | 72 | // `.container` is the outer content wrapper on all three pages, so the three | |
| 73 | 73 | // crops share their left/right margins. | |
| 74 | 74 | // | |
| 75 | - | // FRAMING IS NOT SETTLED. A trial run on 2026-08-07 showed the storefront's item | |
| 76 | - | // grid falling below a 16:10 crop anchored at the page top, but the page it was | |
| 77 | - | // shot against had placeholder covers and three items in a five-wide grid. Both | |
| 78 | - | // change when the media (GoingsOn ed68814e) and the wider catalog (32f1b8d1) | |
| 79 | - | // land, and re-tuning against the current content would be fitting to a state | |
| 80 | - | // that is about to move. So the knobs are env vars rather than edits: set | |
| 81 | - | // CAPTURE_ASPECT, or a frame's `selector`, once there is real content to judge. | |
| 75 | + | // FRAMING, decided 2026-08-07: frame 1 shows the storefront header *and* the | |
| 76 | + | // first row of items. "Your storefront: sell anything digital" is a claim about | |
| 77 | + | // a shop, and a crop holding only the title and blurb could be any page; a crop | |
| 78 | + | // holding only the grid loses whose shop it is. Showing both costs a taller | |
| 79 | + | // frame than 16:10, which is why the ratio is 3:2 rather than the wider crop the | |
| 80 | + | // first draft used. | |
| 81 | + | // | |
| 82 | + | // The knobs stay env vars so a re-tune after a design change is a flag, not an | |
| 83 | + | // edit: CAPTURE_ASPECT, CAPTURE_WIDTH, and a per-frame `selector`. | |
| 82 | 84 | const DEFAULT_SELECTOR = '.container'; | |
| 83 | 85 | ||
| 84 | 86 | /** |
| @@ -143,6 +143,41 @@ | |||
| 143 | 143 | ||
| 144 | 144 | Read slowly. Read the same page twice. The commons is patient, and so are its books."; | |
| 145 | 145 | ||
| 146 | + | /// Body for "Marginal Notes: On Editions". Original prose, same as above. | |
| 147 | + | const ON_EDITIONS_BODY: &str = "\ | |
| 148 | + | An edition is an argument about how a text should be met. Where the line breaks, \ | |
| 149 | + | how wide the margin runs, whether a note sits at the foot of the page or is exiled \ | |
| 150 | + | to the back: each is a claim about what the reader is for. | |
| 151 | + | ||
| 152 | + | The claims we make here are modest. Long measure tires the eye, so the measure is \ | |
| 153 | + | short. Notes belong where the sentence that needs them is, so they stay on the page. \ | |
| 154 | + | Nothing is set in a face that wants to be admired. | |
| 155 | + | ||
| 156 | + | A good edition disappears. You should finish it remembering the book."; | |
| 157 | + | ||
| 158 | + | /// Body for "The Reader, No. 1". | |
| 159 | + | const READER_ONE_BODY: &str = "\ | |
| 160 | + | The first number collects three short pieces from the commons, chosen for how they \ | |
| 161 | + | sit next to each other rather than for any theme. | |
| 162 | + | ||
| 163 | + | The oldest is a traveller's account, written by someone with no gift for landscape \ | |
| 164 | + | and an unusual ear for how people speak. The second is a letter that was never sent. \ | |
| 165 | + | The third is a fragment, and it stops where the manuscript does. | |
| 166 | + | ||
| 167 | + | Fragments are worth setting properly. A text that breaks off mid-thought is not a \ | |
| 168 | + | failure of the text."; | |
| 169 | + | ||
| 170 | + | /// Body for "The Reader, No. 2". | |
| 171 | + | const READER_TWO_BODY: &str = "\ | |
| 172 | + | Two essays this month, both on work, both by people who did not think of themselves \ | |
| 173 | + | as writers. | |
| 174 | + | ||
| 175 | + | The first was set down by a printer late in a long career, and reads like someone \ | |
| 176 | + | explaining a trade to a nephew who has not asked. The second is angrier and shorter, \ | |
| 177 | + | and was published anonymously, which was the ordinary caution of the time. | |
| 178 | + | ||
| 179 | + | Neither has been edited for taste. The spelling is regularised and nothing else."; | |
| 180 | + | ||
| 146 | 181 | /// The five content creators. Their projects span all four `PricingKind`s and, | |
| 147 | 182 | /// across their items, every `ItemType`. See the sprint doc | |
| 148 | 183 | /// `_private/docs/mnw/testnot-example-seed.md`. | |
| @@ -210,6 +245,28 @@ | |||
| 210 | 245 | media: Some("session-take-video"), | |
| 211 | 246 | cover: Some("session-take-cover"), | |
| 212 | 247 | }, | |
| 248 | + | ItemSpec { | |
| 249 | + | title: "Restoration No. 2 (Full Mix)", | |
| 250 | + | description: "The second restoration in the volume, from a \ | |
| 251 | + | later reel. Name your price.", | |
| 252 | + | item_type: ItemType::Audio, | |
| 253 | + | pricing: ItemPricing::Pwyw { min: 100 }, | |
| 254 | + | tags: &["audio", "audio.format.music"], | |
| 255 | + | body: None, | |
| 256 | + | media: Some("restoration-2-audio"), | |
| 257 | + | cover: Some("restoration-2-cover"), | |
| 258 | + | }, | |
| 259 | + | ItemSpec { | |
| 260 | + | title: "Stem Pack: Brass", | |
| 261 | + | description: "Isolated brass stems from the second restoration. \ | |
| 262 | + | WAV, ready for your DAW.", | |
| 263 | + | item_type: ItemType::Sample, | |
| 264 | + | pricing: ItemPricing::Pwyw { min: 200 }, | |
| 265 | + | tags: &["audio.format.samples", "audio.technique.sampling"], | |
| 266 | + | body: None, | |
| 267 | + | media: None, | |
| 268 | + | cover: Some("stem-pack-brass-cover"), | |
| 269 | + | }, | |
| 213 | 270 | ], | |
| 214 | 271 | }, | |
| 215 | 272 | }, | |
| @@ -292,6 +349,17 @@ | |||
| 292 | 349 | media: None, | |
| 293 | 350 | cover: Some("deskriver-utility-cover"), | |
| 294 | 351 | }, | |
| 352 | + | ItemSpec { | |
| 353 | + | title: "Deskriver Notes (Plugin)", | |
| 354 | + | description: "A plain-text notes panel for the suite. Local \ | |
| 355 | + | files, no database, no sync.", | |
| 356 | + | item_type: ItemType::Plugin, | |
| 357 | + | pricing: ItemPricing::BuyOnce(800), | |
| 358 | + | tags: &["software.format.plugin", "software.platform.macos"], | |
| 359 | + | body: None, | |
| 360 | + | media: None, | |
| 361 | + | cover: Some("deskriver-notes-cover"), | |
| 362 | + | }, | |
| 295 | 363 | ], | |
| 296 | 364 | }, | |
| 297 | 365 | }, | |
| @@ -321,17 +389,63 @@ | |||
| 321 | 389 | features: &["downloads"], | |
| 322 | 390 | pricing: ProjectPricing::Free, | |
| 323 | 391 | tiers: &[], | |
| 324 | - | items: &[ItemSpec { | |
| 325 | - | title: "Field Study 01 (Print)", | |
| 326 | - | description: "A high-resolution landscape study, released CC0. \ | |
| 327 | - | Free to download, print, and remix.", | |
| 328 | - | item_type: ItemType::Image, | |
| 329 | - | pricing: ItemPricing::Free, | |
| 330 | - | tags: &["visual.medium.photography", "visual"], | |
| 331 | - | body: None, | |
| 332 | - | media: None, | |
| 333 | - | cover: Some("field-study-01-cover"), | |
| 334 | - | }], | |
| 392 | + | items: &[ | |
| 393 | + | ItemSpec { | |
| 394 | + | title: "Field Study 01 (Print)", | |
| 395 | + | description: "A high-resolution landscape study, released CC0. \ | |
| 396 | + | Free to download, print, and remix.", | |
| 397 | + | item_type: ItemType::Image, | |
| 398 | + | pricing: ItemPricing::Free, | |
| 399 | + | tags: &["visual.medium.photography", "visual"], | |
| 400 | + | body: None, | |
| 401 | + | media: None, | |
| 402 | + | cover: Some("field-study-01-cover"), | |
| 403 | + | }, | |
| 404 | + | ItemSpec { | |
| 405 | + | title: "Field Study 02 (Print)", | |
| 406 | + | description: "A second landscape study from the same series. \ | |
| 407 | + | Full resolution, CC0.", | |
| 408 | + | item_type: ItemType::Image, | |
| 409 | + | pricing: ItemPricing::Free, | |
| 410 | + | tags: &["visual.medium.photography", "visual"], | |
| 411 | + | body: None, | |
| 412 | + | media: None, | |
| 413 | + | cover: Some("field-study-02-cover"), | |
| 414 | + | }, | |
| 415 | + | ItemSpec { | |
| 416 | + | title: "Field Study 03 (Print)", | |
| 417 | + | description: "Late light over open ground. Full resolution, \ | |
| 418 | + | CC0, no attribution required.", | |
| 419 | + | item_type: ItemType::Image, | |
| 420 | + | pricing: ItemPricing::Free, | |
| 421 | + | tags: &["visual.medium.photography", "visual"], | |
| 422 | + | body: None, | |
| 423 | + | media: None, | |
| 424 | + | cover: Some("field-study-03-cover"), | |
| 425 | + | }, | |
| 426 | + | ItemSpec { | |
| 427 | + | title: "Still Life 01 (Print)", | |
| 428 | + | description: "A quiet interior arrangement, shot in daylight. \ | |
| 429 | + | Full resolution, CC0.", | |
| 430 | + | item_type: ItemType::Image, | |
| 431 | + | pricing: ItemPricing::Free, | |
| 432 | + | tags: &["visual.medium.photography", "visual"], | |
| 433 | + | body: None, | |
| 434 | + | media: None, | |
| 435 | + | cover: Some("still-life-01-cover"), | |
| 436 | + | }, | |
| 437 | + | ItemSpec { | |
| 438 | + | title: "Still Life 02 (Print)", | |
| 439 | + | description: "The same table, a different hour. Full \ | |
| 440 | + | resolution, CC0.", | |
| 441 | + | item_type: ItemType::Image, | |
| 442 | + | pricing: ItemPricing::Free, | |
| 443 | + | tags: &["visual.medium.photography", "visual"], | |
| 444 | + | body: None, | |
| 445 | + | media: None, | |
| 446 | + | cover: Some("still-life-02-cover"), | |
| 447 | + | }, | |
| 448 | + | ], | |
| 335 | 449 | }, | |
| 336 | 450 | }, | |
| 337 | 451 | CreatorSpec { | |
| @@ -400,6 +514,39 @@ | |||
| 400 | 514 | media: None, | |
| 401 | 515 | cover: Some("typesetting-commons-cover"), | |
| 402 | 516 | }, | |
| 517 | + | ItemSpec { | |
| 518 | + | title: "The Reader, No. 1", | |
| 519 | + | description: "The first monthly number: three short pieces from \ | |
| 520 | + | the commons, newly set.", | |
| 521 | + | item_type: ItemType::Text, | |
| 522 | + | pricing: ItemPricing::Free, | |
| 523 | + | tags: &["writing.format.essay", "writing"], | |
| 524 | + | body: Some(READER_ONE_BODY), | |
| 525 | + | media: None, | |
| 526 | + | cover: Some("reader-one-cover"), | |
| 527 | + | }, | |
| 528 | + | ItemSpec { | |
| 529 | + | title: "The Reader, No. 2", | |
| 530 | + | description: "Two essays on work, by people who did not think of \ | |
| 531 | + | themselves as writers.", | |
| 532 | + | item_type: ItemType::Text, | |
| 533 | + | pricing: ItemPricing::Free, | |
| 534 | + | tags: &["writing.format.essay", "writing"], | |
| 535 | + | body: Some(READER_TWO_BODY), | |
| 536 | + | media: None, | |
| 537 | + | cover: Some("reader-two-cover"), | |
| 538 | + | }, | |
| 539 | + | ItemSpec { | |
| 540 | + | title: "Marginal Notes: On Editions", | |
| 541 | + | description: "What a margin, a measure, and a footnote each \ | |
| 542 | + | claim about the reader.", | |
| 543 | + | item_type: ItemType::Text, | |
| 544 | + | pricing: ItemPricing::Free, | |
| 545 | + | tags: &["writing.format.essay", "writing.topic.creativity"], | |
| 546 | + | body: Some(ON_EDITIONS_BODY), | |
| 547 | + | media: None, | |
| 548 | + | cover: Some("on-editions-cover"), | |
| 549 | + | }, | |
| 403 | 550 | ], | |
| 404 | 551 | }, | |
| 405 | 552 | }, | |
| @@ -430,17 +577,63 @@ | |||
| 430 | 577 | features: &["downloads"], | |
| 431 | 578 | pricing: ProjectPricing::Free, | |
| 432 | 579 | tiers: &[], | |
| 433 | - | items: &[ItemSpec { | |
| 434 | - | title: "Community Bundle Vol. 1", | |
| 435 | - | description: "A free bundle collecting highlights shared through \ | |
| 436 | - | the account, music and art, all freely licensed.", | |
| 437 | - | item_type: ItemType::Bundle, | |
| 438 | - | pricing: ItemPricing::Free, | |
| 439 | - | tags: &["audio", "visual"], | |
| 440 | - | body: None, | |
| 441 | - | media: None, | |
| 442 | - | cover: Some("community-bundle-cover"), | |
| 443 | - | }], | |
| 580 | + | items: &[ | |
| 581 | + | ItemSpec { | |
| 582 | + | title: "Community Bundle Vol. 1", | |
| 583 | + | description: "A free bundle collecting highlights shared through \ | |
| 584 | + | the account, music and art, all freely licensed.", | |
| 585 | + | item_type: ItemType::Bundle, | |
| 586 | + | pricing: ItemPricing::Free, | |
| 587 | + | tags: &["audio", "visual"], | |
| 588 | + | body: None, | |
| 589 | + | media: None, | |
| 590 | + | cover: Some("community-bundle-cover"), | |
| 591 | + | }, | |
| 592 | + | ItemSpec { | |
| 593 | + | title: "Community Bundle Vol. 2", | |
| 594 | + | description: "The second collection. Same terms: free to \ | |
| 595 | + | download, free to pass on.", | |
| 596 | + | item_type: ItemType::Bundle, | |
| 597 | + | pricing: ItemPricing::Free, | |
| 598 | + | tags: &["audio", "visual"], | |
| 599 | + | body: None, | |
| 600 | + | media: None, | |
| 601 | + | cover: Some("community-bundle-2-cover"), | |
| 602 | + | }, | |
| 603 | + | ItemSpec { | |
| 604 | + | title: "Sampler: Field Recordings", | |
| 605 | + | description: "Room tone, weather, and open ground. Freely \ | |
| 606 | + | licensed, for anything you like.", | |
| 607 | + | item_type: ItemType::Sample, | |
| 608 | + | pricing: ItemPricing::Free, | |
| 609 | + | tags: &["audio.format.samples", "audio"], | |
| 610 | + | body: None, | |
| 611 | + | media: None, | |
| 612 | + | cover: Some("field-recordings-cover"), | |
| 613 | + | }, | |
| 614 | + | ItemSpec { | |
| 615 | + | title: "Sampler: Public-Domain Loops", | |
| 616 | + | description: "Short loops cut from public-domain recordings, \ | |
| 617 | + | tempo-labelled and ready to drop in.", | |
| 618 | + | item_type: ItemType::Sample, | |
| 619 | + | pricing: ItemPricing::Free, | |
| 620 | + | tags: &["audio.format.samples", "audio.technique.sampling"], | |
| 621 | + | body: None, | |
| 622 | + | media: None, | |
| 623 | + | cover: Some("pd-loops-cover"), | |
| 624 | + | }, | |
| 625 | + | ItemSpec { | |
| 626 | + | title: "Commons Reader (Download)", | |
| 627 | + | description: "A collected PDF of the writing shared through the \ | |
| 628 | + | account this year.", | |
| 629 | + | item_type: ItemType::Digital, | |
| 630 | + | pricing: ItemPricing::Free, | |
| 631 | + | tags: &["writing", "writing.format.essay"], | |
| 632 | + | body: None, | |
| 633 | + | media: None, | |
| 634 | + | cover: Some("commons-reader-cover"), | |
| 635 | + | }, | |
| 636 | + | ], | |
| 444 | 637 | }, | |
| 445 | 638 | }, | |
| 446 | 639 | ]; |
| @@ -189,7 +189,10 @@ | |||
| 189 | 189 | ||
| 190 | 190 | let client = reqwest::Client::builder() | |
| 191 | 191 | .timeout(FETCH_TIMEOUT) | |
| 192 | - | .user_agent("makenotwork-example-seed") | |
| 192 | + | // Wikimedia's User-Agent policy refuses generic agents outright, and | |
| 193 | + | // some of the manifest is hosted there. Name the project and give a | |
| 194 | + | // contact, which is what the policy asks for. | |
| 195 | + | .user_agent("makenotwork-example-seed/1.0 (+https://makenot.work; info@makenot.work)") | |
| 193 | 196 | .build() | |
| 194 | 197 | .map_err(|e| ManifestError::Unresolved(vec![format!("http client: {e}")]))?; | |
| 195 | 198 | ||
| @@ -405,12 +408,96 @@ | |||
| 405 | 408 | } | |
| 406 | 409 | ||
| 407 | 410 | #[tokio::test] | |
| 408 | - | async fn an_uncurated_manifest_resolves_to_nothing_without_network() { | |
| 409 | - | let manifest = Manifest::parse(EMBEDDED_MANIFEST).unwrap(); | |
| 410 | - | // True until the first asset is curated; the point is that a fully | |
| 411 | - | // uncurated manifest never reaches for the network. | |
| 412 | - | if manifest.assets.iter().all(|a| !a.is_curated()) { | |
| 413 | - | assert!(manifest.resolve().await.unwrap().is_empty()); | |
| 411 | + | async fn an_uncurated_manifest_never_reaches_for_the_network() { | |
| 412 | + | // Synthetic rather than the embedded manifest, which is now fully | |
| 413 | + | // curated: this is a claim about the code path, and it should keep | |
| 414 | + | // holding whatever the shipped manifest looks like. | |
| 415 | + | let text = r#" | |
| 416 | + | [[asset]] | |
| 417 | + | id = "a" | |
| 418 | + | media_type = "image/png" | |
| 419 | + | filename = "a.png" | |
| 420 | + | license = "CC0-1.0" | |
| 421 | + | title = "A" | |
| 422 | + | source = "https://example.test/a" | |
| 423 | + | "#; | |
| 424 | + | let resolved = Manifest::parse(text).unwrap().resolve().await.unwrap(); | |
| 425 | + | assert!(resolved.is_empty()); | |
| 426 | + | } | |
| 427 | + | ||
| 428 | + | #[test] | |
| 429 | + | fn every_shipped_asset_is_curated_and_pinned() { | |
| 430 | + | // An unpinned asset still works, but it re-fetches every run and cannot | |
| 431 | + | // detect the file changing at the source. The shipped manifest was | |
| 432 | + | // curated in one pass with digests taken from the bytes that arrived, | |
| 433 | + | // so anything unpinned here is an entry someone added without running | |
| 434 | + | // it. | |
| 435 | + | let manifest = Manifest::load().expect("manifest loads"); | |
| 436 | + | for asset in &manifest.assets { | |
| 437 | + | assert!(asset.is_curated(), "{} has no url", asset.id); | |
| 438 | + | let digest = asset.sha256.as_deref().unwrap_or_default().trim(); | |
| 439 | + | assert_eq!(digest.len(), 64, "{} is not pinned", asset.id); | |
| 440 | + | assert!( | |
| 441 | + | digest.chars().all(|c| c.is_ascii_hexdigit()), | |
| 442 | + | "{} has a malformed digest", | |
| 443 | + | asset.id | |
| 444 | + | ); | |
| 445 | + | } | |
| 446 | + | } | |
| 447 | + | ||
| 448 | + | /// Fetch every shipped asset and check it against its pin. | |
| 449 | + | /// | |
| 450 | + | /// Ignored by default: it is ~90 MB over the network and depends on two | |
| 451 | + | /// museums staying up, neither of which belongs in a normal test run. Run it | |
| 452 | + | /// after editing the manifest, which is the moment it earns its cost: | |
| 453 | + | /// | |
| 454 | + | /// cargo test --lib seed::manifest -- --ignored --nocapture | |
| 455 | + | #[tokio::test] | |
| 456 | + | #[ignore = "network: fetches every asset in the manifest"] | |
| 457 | + | async fn every_shipped_asset_actually_resolves() { | |
| 458 | + | let manifest = Manifest::load().expect("manifest loads"); | |
| 459 | + | let declared = manifest.assets.len(); | |
| 460 | + | let resolved = manifest | |
| 461 | + | .resolve() | |
| 462 | + | .await | |
| 463 | + | .expect("every asset should resolve"); | |
| 464 | + | assert_eq!( | |
| 465 | + | resolved.len(), | |
| 466 | + | declared, | |
| 467 | + | "resolved {} of {declared} assets", | |
| 468 | + | resolved.len() | |
| 469 | + | ); | |
| 470 | + | assert!(!resolved.attribution_markdown().is_empty()); | |
| 471 | + | } | |
| 472 | + | ||
| 473 | + | #[test] | |
| 474 | + | fn every_shipped_asset_is_public_domain_and_verifiable() { | |
| 475 | + | // Rule 1 and rule 2 of the manifest header, enforced rather than | |
| 476 | + | // trusted. These files sit on a public box under a licence claim this | |
| 477 | + | // repo makes; a CC-BY asset slipping in is a licensing problem, and a | |
| 478 | + | // `source` that is not a reachable page makes the claim uncheckable. | |
| 479 | + | let manifest = Manifest::load().expect("manifest loads"); | |
| 480 | + | for asset in &manifest.assets { | |
| 481 | + | let licence = asset.license.to_ascii_lowercase(); | |
| 482 | + | assert!( | |
| 483 | + | licence.contains("cc0") || licence.contains("public domain"), | |
| 484 | + | "{} is licensed {:?}, which is not public domain or CC0", | |
| 485 | + | asset.id, | |
| 486 | + | asset.license | |
| 487 | + | ); | |
| 488 | + | assert!( | |
| 489 | + | asset.source.starts_with("https://"), | |
| 490 | + | "{} has no https source page", | |
| 491 | + | asset.id | |
| 492 | + | ); | |
| 493 | + | assert!( | |
| 494 | + | asset | |
| 495 | + | .url | |
| 496 | + | .as_deref() | |
| 497 | + | .is_some_and(|u| u.starts_with("https://")), | |
| 498 | + | "{} is not fetched over https", | |
| 499 | + | asset.id | |
| 500 | + | ); | |
| 414 | 501 | } | |
| 415 | 502 | } | |
| 416 | 503 |
| @@ -24,234 +24,435 @@ | |||
| 24 | 24 | # series across a storefront reads fine). Reusing it across *every* id does | |
| 25 | 25 | # not: a grid of one repeated image is the problem this replaces. | |
| 26 | 26 | # | |
| 27 | - | # SOURCES implied by the locked naming convention (src/seed/creators.rs): | |
| 28 | - | # audio Musopen (https://musopen.org) — PD recordings of PD scores | |
| 29 | - | # texts Project Gutenberg (https://gutenberg.org) | |
| 30 | - | # photography Wikimedia Commons CC0 (https://commons.wikimedia.org) | |
| 27 | + | # WHERE TO LOOK when adding or replacing one. Both of these expose the licence | |
| 28 | + | # as a queryable field, which is why they beat a general image search: you are | |
| 29 | + | # reading the institution's own claim rather than a page that says "free". | |
| 30 | + | # https://collectionapi.metmuseum.org/public/collection/v1/ (isPublicDomain) | |
| 31 | + | # https://commons.wikimedia.org/w/api.php (extmetadata) | |
| 32 | + | # Musopen and Project Gutenberg are also fine and were the original suggestion; | |
| 33 | + | # neither ended up needed. | |
| 31 | 34 | # | |
| 32 | - | # COVERS ARE THE LOAD-BEARING HALF. Sixteen of the nineteen entries below are | |
| 35 | + | # COVERS ARE THE LOAD-BEARING HALF. Thirty of the thirty-four entries below are | |
| 33 | 36 | # cover art, because a storefront of grey squares reads as unfinished however | |
| 34 | - | # many items are in it. The three media files matter less: only Audio, Video and | |
| 37 | + | # many items are in it. The four media files matter less: only Audio, Video and | |
| 35 | 38 | # Image items serve a real file to a visitor. Every other item type downloads a | |
| 36 | 39 | # short text blob, and a demo download that says "example file" is honest rather | |
| 37 | 40 | # than broken, so those are deliberately not declared here. | |
| 41 | + | # | |
| 42 | + | # CURATED 2026-08-07. Every entry below was fetched and its sha256 is the digest | |
| 43 | + | # of the bytes that came back, verified a second time from a cold cache by a | |
| 44 | + | # different HTTP client. A pin here is a fact rather than a promise. | |
| 45 | + | # | |
| 46 | + | # THE MET RE-ENCODES. Twelve of the thirty images changed digest within about | |
| 47 | + | # half an hour of first being pinned: same URL, same object, a few kilobytes | |
| 48 | + | # different. Same bytes on every request since. So expect a mismatch here | |
| 49 | + | # eventually, and do not read one as an attack. The recovery is: | |
| 50 | + | # | |
| 51 | + | # 1. Open the asset's `source` page and confirm it is still the same object | |
| 52 | + | # and still flagged public domain. This is the step the pin exists to | |
| 53 | + | # force; skipping it defeats the whole mechanism. | |
| 54 | + | # 2. Re-run the seed, take the digest it logged, paste it into `sha256`. | |
| 55 | + | # 3. cargo test --lib seed::manifest -- --ignored (fetches everything again) | |
| 56 | + | # | |
| 57 | + | # The alternative, downgrading a mismatch to a warning, was considered and | |
| 58 | + | # rejected: it would turn "this file was replaced" into a line in a log nobody | |
| 59 | + | # reads, on a box whose whole job is to be looked at. | |
| 60 | + | # | |
| 61 | + | # Two sources, both chosen because the licence is machine-readable at the | |
| 62 | + | # source rather than asserted by a human reading a page: | |
| 63 | + | # | |
| 64 | + | # Metropolitan Museum of Art Open Access (the 30 images). Everything the Met | |
| 65 | + | # flags isPublicDomain is released CC0, and every object has a public page. | |
| 66 | + | # Wikimedia Commons (the 4 media files). Public-domain recordings and one | |
| 67 | + | # 1930 orchestra short. | |
| 68 | + | # | |
| 69 | + | # The imagery is period rather than modern on purpose: 19th-century photographs | |
| 70 | + | # for a CC0 field library, instruments for a reissue label, ornament plates for | |
| 71 | + | # a tools suite, and printing history for a press. It reads as a considered | |
| 72 | + | # catalog instead of as stock photography, which is the whole job of a demo. | |
| 38 | 73 | ||
| 39 | - | # ── Project covers ────────────────────────────────────────────────────────── | |
| 40 | - | # One per storefront (5). These are the largest images on the site: they head | |
| 41 | - | # /p/<slug> and appear on /discover. | |
| 74 | + | # ── Open Reels (a reissue label): instruments ───────────────────────────────── | |
| 42 | 75 | ||
| 43 | 76 | [[asset]] | |
| 44 | 77 | id = "openreels-project-cover" | |
| 45 | - | url = "" | |
| 46 | - | sha256 = "" | |
| 78 | + | url = "https://images.metmuseum.org/CRDImages/mi/original/DP302552.jpg" | |
| 79 | + | sha256 = "c74e1ca8c28f7e2ec10fd4af4b322ab87dc71f1639eab26623c764f38f8e9f74" | |
| 47 | 80 | media_type = "image/jpeg" | |
| 48 | - | filename = "openreels-cover.jpg" | |
| 49 | - | license = "" | |
| 50 | - | title = "" | |
| 51 | - | source = "" | |
| 52 | - | # Restored Reels, Vol. 1 — a reissue label. Archival studio or tape imagery. | |
| 53 | - | ||
| 54 | - | [[asset]] | |
| 55 | - | id = "deskriver-project-cover" | |
| 56 | - | url = "" | |
| 57 | - | sha256 = "" | |
| 58 | - | media_type = "image/jpeg" | |
| 59 | - | filename = "deskriver-cover.jpg" | |
| 60 | - | license = "" | |
| 61 | - | title = "" | |
| 62 | - | source = "" | |
| 63 | - | # Deskriver Suite — offline desktop tools. Something spare and geometric; avoid | |
| 64 | - | # stock-photo laptops. | |
| 65 | - | ||
| 66 | - | [[asset]] | |
| 67 | - | id = "stillfield-project-cover" | |
| 68 | - | url = "" | |
| 69 | - | sha256 = "" | |
| 70 | - | media_type = "image/jpeg" | |
| 71 | - | filename = "stillfield-cover.jpg" | |
| 72 | - | license = "" | |
| 73 | - | title = "" | |
| 74 | - | source = "" | |
| 75 | - | # CC0 Field Library — landscape and still-life photography. This one should be | |
| 76 | - | # the strongest image on the box; it is the storefront most likely to be the | |
| 77 | - | # carousel's frame 1. | |
| 78 | - | ||
| 79 | - | [[asset]] | |
| 80 | - | id = "marginalia-project-cover" | |
| 81 | - | url = "" | |
| 82 | - | sha256 = "" | |
| 83 | - | media_type = "image/jpeg" | |
| 84 | - | filename = "marginalia-cover.jpg" | |
| 85 | - | license = "" | |
| 86 | - | title = "" | |
| 87 | - | source = "" | |
| 88 | - | # The Marginalia Reader — a one-person press. Type specimens, letterpress, | |
| 89 | - | # bookbinding. | |
| 90 | - | ||
| 91 | - | [[asset]] | |
| 92 | - | id = "commonshare-project-cover" | |
| 93 | - | url = "" | |
| 94 | - | sha256 = "" | |
| 95 | - | media_type = "image/jpeg" | |
| 96 | - | filename = "commonshare-cover.jpg" | |
| 97 | - | license = "" | |
| 98 | - | title = "" | |
| 99 | - | source = "" | |
| 100 | - | # Commons Sampler — the benefit account. Anything communal and unbranded. | |
| 101 | - | ||
| 102 | - | # ── Item covers ───────────────────────────────────────────────────────────── | |
| 103 | - | # One per item (11), in roster order. | |
| 81 | + | filename = "openreels-project.jpg" | |
| 82 | + | license = "CC0-1.0" | |
| 83 | + | title = "Archlute" | |
| 84 | + | author = "David Tecchler" | |
| 85 | + | source = "https://www.metmuseum.org/art/collection/search/503613" | |
| 104 | 86 | ||
| 105 | 87 | [[asset]] | |
| 106 | 88 | id = "restoration-1-cover" | |
| 107 | - | url = "" | |
| 108 | - | sha256 = "" | |
| 89 | + | url = "https://images.metmuseum.org/CRDImages/mi/original/DP105130.jpg" | |
| 90 | + | sha256 = "dee90828336bc2683a406d362b846e5f79eb09352cc84a1fb2cc784e76f951c3" | |
| 109 | 91 | media_type = "image/jpeg" | |
| 110 | - | filename = "restoration-1-cover.jpg" | |
| 111 | - | license = "" | |
| 112 | - | title = "" | |
| 113 | - | source = "" | |
| 92 | + | filename = "restoration-1.jpg" | |
| 93 | + | license = "CC0-1.0" | |
| 94 | + | title = "\"Antonius\" Violin" | |
| 95 | + | author = "Antonio Stradivari" | |
| 96 | + | source = "https://www.metmuseum.org/art/collection/search/503008" | |
| 114 | 97 | ||
| 115 | 98 | [[asset]] | |
| 116 | 99 | id = "stem-pack-strings-cover" | |
| 117 | - | url = "" | |
| 118 | - | sha256 = "" | |
| 100 | + | url = "https://images.metmuseum.org/CRDImages/mi/original/DP167848.jpg" | |
| 101 | + | sha256 = "116b298709762e3b48b19a1410ae0c3e4606f5f16f2fc107672b45f8e5665d2f" | |
| 119 | 102 | media_type = "image/jpeg" | |
| 120 | - | filename = "stem-pack-strings-cover.jpg" | |
| 121 | - | license = "" | |
| 122 | - | title = "" | |
| 123 | - | source = "" | |
| 103 | + | filename = "stem-pack-strings.jpg" | |
| 104 | + | license = "CC0-1.0" | |
| 105 | + | title = "\"Francesca\" Violin" | |
| 106 | + | author = "Antonio Stradivari" | |
| 107 | + | source = "https://www.metmuseum.org/art/collection/search/503010" | |
| 124 | 108 | ||
| 125 | 109 | [[asset]] | |
| 126 | 110 | id = "session-take-cover" | |
| 127 | - | url = "" | |
| 128 | - | sha256 = "" | |
| 111 | + | url = "https://images.metmuseum.org/CRDImages/mi/original/DP302551.jpg" | |
| 112 | + | sha256 = "1b516d474fcc8d56191c83a5209a7c98dd07453bf27a31fd1992e1324a6dff38" | |
| 129 | 113 | media_type = "image/jpeg" | |
| 130 | - | filename = "session-take-cover.jpg" | |
| 131 | - | license = "" | |
| 132 | - | title = "" | |
| 133 | - | source = "" | |
| 114 | + | filename = "session-take.jpg" | |
| 115 | + | license = "CC0-1.0" | |
| 116 | + | title = "Cittern" | |
| 117 | + | author = "Joachim Tielke" | |
| 118 | + | source = "https://www.metmuseum.org/art/collection/search/504210" | |
| 119 | + | ||
| 120 | + | [[asset]] | |
| 121 | + | id = "restoration-2-cover" | |
| 122 | + | url = "https://images.metmuseum.org/CRDImages/mi/original/DT679.jpg" | |
| 123 | + | sha256 = "582000673ef0f603fd7791358b595b16fc81b0d1d20a60abd7dec751adb32eb7" | |
| 124 | + | media_type = "image/jpeg" | |
| 125 | + | filename = "restoration-2.jpg" | |
| 126 | + | license = "CC0-1.0" | |
| 127 | + | title = "Mandolin" | |
| 128 | + | author = "Angelo Mannello" | |
| 129 | + | source = "https://www.metmuseum.org/art/collection/search/503675" | |
| 130 | + | ||
| 131 | + | [[asset]] | |
| 132 | + | id = "stem-pack-brass-cover" | |
| 133 | + | url = "https://images.metmuseum.org/CRDImages/mi/original/DP318718.jpg" | |
| 134 | + | sha256 = "d3a841f045dceb352dd51d06f130ed0c03b0ad92df5114e702a274d2ca8acde6" | |
| 135 | + | media_type = "image/jpeg" | |
| 136 | + | filename = "stem-pack-brass.jpg" | |
| 137 | + | license = "CC0-1.0" | |
| 138 | + | title = "Alto Recorder in F" | |
| 139 | + | author = "Johann Benedikt Gahn" | |
| 140 | + | source = "https://www.metmuseum.org/art/collection/search/501520" | |
| 141 | + | ||
| 142 | + | # ── Deskriver (desktop tools): ornament and pattern design ──────────────────── | |
| 143 | + | ||
| 144 | + | [[asset]] | |
| 145 | + | id = "deskriver-project-cover" | |
| 146 | + | url = "https://images.metmuseum.org/CRDImages/dp/original/DP801363.jpg" | |
| 147 | + | sha256 = "256c0ba3fb2509cd56db6c47dd87e1499c83e3fa78ddfa22dccc54984c8491c5" | |
| 148 | + | media_type = "image/jpeg" | |
| 149 | + | filename = "deskriver-project.jpg" | |
| 150 | + | license = "CC0-1.0" | |
| 151 | + | title = "Design for a Floral Pattern" | |
| 152 | + | author = "Christopher Dresser" | |
| 153 | + | source = "https://www.metmuseum.org/art/collection/search/363718" | |
| 134 | 154 | ||
| 135 | 155 | [[asset]] | |
| 136 | 156 | id = "deskriver-focus-cover" | |
| 137 | - | url = "" | |
| 138 | - | sha256 = "" | |
| 157 | + | url = "https://images.metmuseum.org/CRDImages/dp/original/DP804627.jpg" | |
| 158 | + | sha256 = "007deb54058cf6284b0abee22017e926a082b668807acea3d1720474dc4b9e19" | |
| 139 | 159 | media_type = "image/jpeg" | |
| 140 | - | filename = "deskriver-focus-cover.jpg" | |
| 141 | - | license = "" | |
| 142 | - | title = "" | |
| 143 | - | source = "" | |
| 160 | + | filename = "deskriver-focus.jpg" | |
| 161 | + | license = "CC0-1.0" | |
| 162 | + | title = "Design" | |
| 163 | + | author = "Charles Hindley and Sons" | |
| 164 | + | source = "https://www.metmuseum.org/art/collection/search/367270" | |
| 144 | 165 | ||
| 145 | 166 | [[asset]] | |
| 146 | 167 | id = "deskriver-presets-cover" | |
| 147 | - | url = "" | |
| 148 | - | sha256 = "" | |
| 168 | + | url = "https://images.metmuseum.org/CRDImages/dp/original/DP804629.jpg" | |
| 169 | + | sha256 = "f7498261c5b90ffa33bda597ebd340889e2c3d6781d434586c0d401feef78480" | |
| 149 | 170 | media_type = "image/jpeg" | |
| 150 | - | filename = "deskriver-presets-cover.jpg" | |
| 151 | - | license = "" | |
| 152 | - | title = "" | |
| 153 | - | source = "" | |
| 171 | + | filename = "deskriver-presets.jpg" | |
| 172 | + | license = "CC0-1.0" | |
| 173 | + | title = "Design" | |
| 174 | + | author = "Charles Hindley and Sons" | |
| 175 | + | source = "https://www.metmuseum.org/art/collection/search/367273" | |
| 154 | 176 | ||
| 155 | 177 | [[asset]] | |
| 156 | 178 | id = "deskriver-template-cover" | |
| 157 | - | url = "" | |
| 158 | - | sha256 = "" | |
| 179 | + | url = "https://images.metmuseum.org/CRDImages/dp/original/DP804628.jpg" | |
| 180 | + | sha256 = "894790c6792382949796d5a4e25b7e43779925a3022ab3556fe3f72bb101052d" | |
| 159 | 181 | media_type = "image/jpeg" | |
| 160 | - | filename = "deskriver-template-cover.jpg" | |
| 161 | - | license = "" | |
| 162 | - | title = "" | |
| 163 | - | source = "" | |
| 182 | + | filename = "deskriver-template.jpg" | |
| 183 | + | license = "CC0-1.0" | |
| 184 | + | title = "Design" | |
| 185 | + | author = "Charles Hindley and Sons" | |
| 186 | + | source = "https://www.metmuseum.org/art/collection/search/367271" | |
| 164 | 187 | ||
| 165 | 188 | [[asset]] | |
| 166 | 189 | id = "deskriver-utility-cover" | |
| 167 | - | url = "" | |
| 168 | - | sha256 = "" | |
| 190 | + | url = "https://images.metmuseum.org/CRDImages/dp/original/DP816472.jpg" | |
| 191 | + | sha256 = "9a94b4860a3011dfdbbbeaf65e7d059603856b196cc9ac09e4f5c11c6d5127dd" | |
| 169 | 192 | media_type = "image/jpeg" | |
| 170 | - | filename = "deskriver-utility-cover.jpg" | |
| 171 | - | license = "" | |
| 172 | - | title = "" | |
| 173 | - | source = "" | |
| 193 | + | filename = "deskriver-utility.jpg" | |
| 194 | + | license = "CC0-1.0" | |
| 195 | + | title = "Embroidery Pattern with Seven Six-pointed Stars and Four Corner Pieces" | |
| 196 | + | author = "Albrecht Dürer" | |
| 197 | + | source = "https://www.metmuseum.org/art/collection/search/388376" | |
| 198 | + | ||
| 199 | + | [[asset]] | |
| 200 | + | id = "deskriver-notes-cover" | |
| 201 | + | url = "https://images.metmuseum.org/CRDImages/dp/original/DP-41178-001.jpg" | |
| 202 | + | sha256 = "b2eb24812e30d0b1b94e148e1ee05852cccf7b3acb93eb889c289f2dbbab0a3a" | |
| 203 | + | media_type = "image/jpeg" | |
| 204 | + | filename = "deskriver-notes.jpg" | |
| 205 | + | license = "CC0-1.0" | |
| 206 | + | title = "The Censer" | |
| 207 | + | author = "Martin Schongauer" | |
| 208 | + | source = "https://www.metmuseum.org/art/collection/search/336167" | |
| 209 | + | ||
| 210 | + | # ── Stillfield (a CC0 photo library): these are the works, not just covers ──── | |
| 211 | + | ||
| 212 | + | [[asset]] | |
| 213 | + | id = "stillfield-project-cover" | |
| 214 | + | url = "https://images.metmuseum.org/CRDImages/ph/original/DP206317.jpg" | |
| 215 | + | sha256 = "f943d8128d935380b5fbd54fda7c58277b74be5bfbaf412710ce614ad4e89750" | |
| 216 | + | media_type = "image/jpeg" | |
| 217 | + | filename = "stillfield-project.jpg" | |
| 218 | + | license = "CC0-1.0" | |
| 219 | + | title = "Life and Landscape on the Norfolk Broads" | |
| 220 | + | author = "Peter Henry Emerson" | |
| 221 | + | source = "https://www.metmuseum.org/art/collection/search/290484" | |
| 174 | 222 | ||
| 175 | 223 | [[asset]] | |
| 176 | 224 | id = "field-study-01-cover" | |
| 177 | - | url = "" | |
| 178 | - | sha256 = "" | |
| 225 | + | url = "https://images.metmuseum.org/CRDImages/ph/original/DP107953.jpg" | |
| 226 | + | sha256 = "70ea0288d11cb1ed6d1434ab65aac2c968a587ae6705887da55c749d014d898c" | |
| 179 | 227 | media_type = "image/jpeg" | |
| 180 | 228 | filename = "field-study-01.jpg" | |
| 181 | - | license = "" | |
| 182 | - | title = "" | |
| 183 | - | source = "" | |
| 184 | - | # This one is both the cover and the work: "Field Study 01 (Print)" is an Image | |
| 185 | - | # item, so the cover is what a buyer downloads. Use a full-resolution file, not | |
| 186 | - | # a thumbnail. | |
| 229 | + | license = "CC0-1.0" | |
| 230 | + | title = "[Landscape with Clouds]" | |
| 231 | + | author = "Roger Fenton" | |
| 232 | + | source = "https://www.metmuseum.org/art/collection/search/282040" | |
| 233 | + | ||
| 234 | + | [[asset]] | |
| 235 | + | id = "field-study-02-cover" | |
| 236 | + | url = "https://images.metmuseum.org/CRDImages/ph/original/DT1150.jpg" | |
| 237 | + | sha256 = "4ad35ea0a181966e5293510583fc0f8eae6cbc557863fa45a65922d50ea68d1f" | |
| 238 | + | media_type = "image/jpeg" | |
| 239 | + | filename = "field-study-02.jpg" | |
| 240 | + | license = "CC0-1.0" | |
| 241 | + | title = "[Landscape with Cottage]" | |
| 242 | + | author = "Marie-Charles-Isidore Choiselat" | |
| 243 | + | source = "https://www.metmuseum.org/art/collection/search/267087" | |
| 244 | + | ||
| 245 | + | [[asset]] | |
| 246 | + | id = "field-study-03-cover" | |
| 247 | + | url = "https://images.metmuseum.org/CRDImages/ph/original/DT1173.jpg" | |
| 248 | + | sha256 = "2d04150aef7d3c6759296071feb1401dc5c549d3484a7e89463812af1fa9f6ce" | |
| 249 | + | media_type = "image/jpeg" | |
| 250 | + | filename = "field-study-03.jpg" | |
| 251 | + | license = "CC0-1.0" | |
| 252 | + | title = "View on the Columbia, Cascades" | |
| 253 | + | author = "Carleton E. Watkins" | |
| 254 | + | source = "https://www.metmuseum.org/art/collection/search/262612" | |
| 255 | + | ||
| 256 | + | [[asset]] | |
| 257 | + | id = "still-life-01-cover" | |
| 258 | + | url = "https://images.metmuseum.org/CRDImages/ph/original/DP109566.jpg" | |
| 259 | + | sha256 = "7933740cf05d19fd556def696022594216369daa167a543ace18647c9dc6409c" | |
| 260 | + | media_type = "image/jpeg" | |
| 261 | + | filename = "still-life-01.jpg" | |
| 262 | + | license = "CC0-1.0" | |
| 263 | + | title = "[Table Top Still Life with Model Cathedral and Small Sculptures]" | |
| 264 | + | author = "Louis-Rémy Robert" | |
| 265 | + | source = "https://www.metmuseum.org/art/collection/search/265090" | |
| 266 | + | ||
| 267 | + | [[asset]] | |
| 268 | + | id = "still-life-02-cover" | |
| 269 | + | url = "https://images.metmuseum.org/CRDImages/ph/original/DP214798.jpg" | |
| 270 | + | sha256 = "02a9f7a5f68012e8e2fb4cdf2eb1ee94ef63fb9f9db7b75a03a32e99ce495c2b" | |
| 271 | + | media_type = "image/jpeg" | |
| 272 | + | filename = "still-life-02.jpg" | |
| 273 | + | license = "CC0-1.0" | |
| 274 | + | title = "[Still Life]" | |
| 275 | + | author = "Louis-Rémy Robert" | |
| 276 | + | source = "https://www.metmuseum.org/art/collection/search/286248" | |
| 277 | + | ||
| 278 | + | # ── Marginalia Press: printing, type and books ──────────────────────────────── | |
| 279 | + | ||
| 280 | + | [[asset]] | |
| 281 | + | id = "marginalia-project-cover" | |
| 282 | + | url = "https://images.metmuseum.org/CRDImages/dp/original/DP102816.jpg" | |
| 283 | + | sha256 = "777e57a47c58f0e061522de52a6c0b7a34ccedb04935ddd99603e50dfd70a536" | |
| 284 | + | media_type = "image/jpeg" | |
| 285 | + | filename = "marginalia-project.jpg" | |
| 286 | + | license = "CC0-1.0" | |
| 287 | + | title = "Hypnerotomachia Poliphili" | |
| 288 | + | author = "Francesco Colonna" | |
| 289 | + | source = "https://www.metmuseum.org/art/collection/search/365313" | |
| 187 | 290 | ||
| 188 | 291 | [[asset]] | |
| 189 | 292 | id = "on-slow-reading-cover" | |
| 190 | - | url = "" | |
| 191 | - | sha256 = "" | |
| 293 | + | url = "https://images.metmuseum.org/CRDImages/dp/original/DP-41182-001.jpg" | |
| 294 | + | sha256 = "3f36ef3af2e1ae450647f587a2c674c05f7f40e0625fdff244d2875b79c32a99" | |
| 192 | 295 | media_type = "image/jpeg" | |
| 193 | - | filename = "on-slow-reading-cover.jpg" | |
| 194 | - | license = "" | |
| 195 | - | title = "" | |
| 196 | - | source = "" | |
| 296 | + | filename = "on-slow-reading.jpg" | |
| 297 | + | license = "CC0-1.0" | |
| 298 | + | title = "Underweysung der Messung (Nuremberg, 1538)" | |
| 299 | + | author = "Albrecht Dürer" | |
| 300 | + | source = "https://www.metmuseum.org/art/collection/search/336657" | |
| 197 | 301 | ||
| 198 | 302 | [[asset]] | |
| 199 | 303 | id = "typesetting-commons-cover" | |
| 200 | - | url = "" | |
| 201 | - | sha256 = "" | |
| 304 | + | url = "https://images.metmuseum.org/CRDImages/dp/original/DP-44771-001.jpg" | |
| 305 | + | sha256 = "d60c215e1116b4b0888f24654958a4ddee61d1494c5a1be131c255b161aa4df3" | |
| 202 | 306 | media_type = "image/jpeg" | |
| 203 | - | filename = "typesetting-commons-cover.jpg" | |
| 204 | - | license = "" | |
| 205 | - | title = "" | |
| 206 | - | source = "" | |
| 307 | + | filename = "typesetting-commons.jpg" | |
| 308 | + | license = "CC0-1.0" | |
| 309 | + | title = "Royal American Magazine, or Universal Repository of Instruction and Amusement, Vol. I" | |
| 310 | + | author = "Isaiah Thomas" | |
| 311 | + | source = "https://www.metmuseum.org/art/collection/search/356473" | |
| 312 | + | ||
| 313 | + | [[asset]] | |
| 314 | + | id = "reader-one-cover" | |
| 315 | + | url = "https://images.metmuseum.org/CRDImages/dp/original/DP-15938-001.jpg" | |
| 316 | + | sha256 = "5a421c66a45a144068e725709f0ba8fbc96554b45dcef6e3413a823ad3aa0538" | |
| 317 | + | media_type = "image/jpeg" | |
| 318 | + | filename = "reader-one.jpg" | |
| 319 | + | license = "CC0-1.0" | |
| 320 | + | title = "The Book of Chess" | |
| 321 | + | author = "Jacobus de Cessolis" | |
| 322 | + | source = "https://www.metmuseum.org/art/collection/search/749108" | |
| 323 | + | ||
| 324 | + | [[asset]] | |
| 325 | + | id = "reader-two-cover" | |
| 326 | + | url = "https://images.metmuseum.org/CRDImages/dp/original/DP841130.jpg" | |
| 327 | + | sha256 = "719bd3f628917796fe6d782c642c56fbc7cc313ff97d05493bd7be13c6136c89" | |
| 328 | + | media_type = "image/jpeg" | |
| 329 | + | filename = "reader-two.jpg" | |
| 330 | + | license = "CC0-1.0" | |
| 331 | + | title = "New Inventions of Modern Times [Nova Reperta], The Invention of Book Printing, plate 4" | |
| 332 | + | author = "Jan Collaert I" | |
| 333 | + | source = "https://www.metmuseum.org/art/collection/search/659683" | |
| 334 | + | ||
| 335 | + | [[asset]] | |
| 336 | + | id = "on-editions-cover" | |
| 337 | + | url = "https://images.metmuseum.org/CRDImages/dp/original/271218.jpg" | |
| 338 | + | sha256 = "4090bb8681e6757c3d738f7f8fd41d298acdf3151dc2d95347dbc6ef8ea85a26" | |
| 339 | + | media_type = "image/jpeg" | |
| 340 | + | filename = "on-editions.jpg" | |
| 341 | + | license = "CC0-1.0" | |
| 342 | + | title = "An Apostle" | |
| 343 | + | author = "Angiolo Falconetto" | |
| 344 | + | source = "https://www.metmuseum.org/art/collection/search/362533" | |
| 345 | + | ||
| 346 | + | # ── Commonshare (the benefit account): a spread of still lifes ──────────────── | |
| 347 | + | ||
| 348 | + | [[asset]] | |
| 349 | + | id = "commonshare-project-cover" | |
| 350 | + | url = "https://images.metmuseum.org/CRDImages/ph/original/DP209243.jpg" | |
| 351 | + | sha256 = "1a17f44d64c3d5199845b3ccefc2c79890eb822e2002ed034ccd0de3c53235d7" | |
| 352 | + | media_type = "image/jpeg" | |
| 353 | + | filename = "commonshare-project.jpg" | |
| 354 | + | license = "CC0-1.0" | |
| 355 | + | title = "Still Life and Embroidery" | |
| 356 | + | author = "Robert Wilfred Skeffington Lutwidge" | |
| 357 | + | source = "https://www.metmuseum.org/art/collection/search/270836" | |
| 207 | 358 | ||
| 208 | 359 | [[asset]] | |
| 209 | 360 | id = "community-bundle-cover" | |
| 210 | - | url = "" | |
| 211 | - | sha256 = "" | |
| 361 | + | url = "https://images.metmuseum.org/CRDImages/ph/original/DP209008.jpg" | |
| 362 | + | sha256 = "8fe4054da9c2497d9903b876c07d9a9b66a66c1a0c3a98c646cd453a494ff4a8" | |
| 212 | 363 | media_type = "image/jpeg" | |
| 213 | - | filename = "community-bundle-cover.jpg" | |
| 214 | - | license = "" | |
| 215 | - | title = "" | |
| 216 | - | source = "" | |
| 364 | + | filename = "community-bundle.jpg" | |
| 365 | + | license = "CC0-1.0" | |
| 366 | + | title = "[Still Life with Fruit]" | |
| 367 | + | author = "Roger Fenton" | |
| 368 | + | source = "https://www.metmuseum.org/art/collection/search/283087" | |
| 217 | 369 | ||
| 218 | - | # ── Media files ───────────────────────────────────────────────────────────── | |
| 219 | - | # The three items that serve a real file to a visitor. Audio has to be actually | |
| 220 | - | # playable: the player is a surface visitors touch, and a silent WAV in it reads | |
| 221 | - | # as a broken site rather than an unfinished one. | |
| 370 | + | [[asset]] | |
| 371 | + | id = "community-bundle-2-cover" | |
| 372 | + | url = "https://images.metmuseum.org/CRDImages/ph/original/DP275754.jpg" | |
| 373 | + | sha256 = "20e38ed186bef5767caed97d14b7314a9ab5ece925429344e6b5ba96f7975524" | |
| 374 | + | media_type = "image/jpeg" | |
| 375 | + | filename = "community-bundle-2.jpg" | |
| 376 | + | license = "CC0-1.0" | |
| 377 | + | title = "[Still Life with Game Birds]" | |
| 378 | + | author = "Charles Nègre" | |
| 379 | + | source = "https://www.metmuseum.org/art/collection/search/286169" | |
| 380 | + | ||
| 381 | + | [[asset]] | |
| 382 | + | id = "field-recordings-cover" | |
| 383 | + | url = "https://images.metmuseum.org/CRDImages/ph/original/DP700250.jpg" | |
| 384 | + | sha256 = "21939590d26b68763899d57ea0a15df62da521cb34dee201a264e901a7da2124" |
Lines truncated
| @@ -26,8 +26,13 @@ | |||
| 26 | 26 | ||
| 27 | 27 | /// The Phase-1 roster size (see `seed::creators::ROSTER`). | |
| 28 | 28 | const SEEDED_CREATORS: i64 = 5; | |
| 29 | - | /// Phase-2 item count: one per `ItemType` spread across the projects. | |
| 30 | - | const SEEDED_ITEMS: i64 = 11; | |
| 29 | + | /// Phase-2 item count: five per project, spanning every `ItemType`. Widened | |
| 30 | + | /// from eleven on 2026-08-07 so the storefront grids read as grids. | |
| 31 | + | const SEEDED_ITEMS: i64 = 25; | |
| 32 | + | ||
| 33 | + | /// Every `ItemType` must still appear at least once across the catalog. Held | |
| 34 | + | /// separately from the item count now that projects carry several of a type. | |
| 35 | + | const ITEM_TYPES: i64 = 11; | |
| 31 | 36 | ||
| 32 | 37 | /// Guard-passing options for a test run: opt-in on, approved example host. | |
| 33 | 38 | fn testnot_opts() -> SeedOptions { | |
| @@ -156,7 +161,7 @@ | |||
| 156 | 161 | .await | |
| 157 | 162 | .expect("seed"); | |
| 158 | 163 | ||
| 159 | - | // One item per ItemType (11), and every type represented exactly once. | |
| 164 | + | // Five items per project, and every ItemType still represented. | |
| 160 | 165 | assert_eq!(count_example_items(&db.pool).await, SEEDED_ITEMS); | |
| 161 | 166 | let distinct_types: i64 = sqlx::query_scalar( | |
| 162 | 167 | "SELECT COUNT(DISTINCT i.item_type) FROM items i \ | |
| @@ -167,7 +172,7 @@ | |||
| 167 | 172 | .fetch_one(&db.pool) | |
| 168 | 173 | .await | |
| 169 | 174 | .expect("distinct item types"); | |
| 170 | - | assert_eq!(distinct_types, SEEDED_ITEMS, "every ItemType should appear"); | |
| 175 | + | assert_eq!(distinct_types, ITEM_TYPES, "every ItemType should appear"); | |
| 171 | 176 | ||
| 172 | 177 | // Phase-2 boundary: nothing is discover-visible yet (all pending). | |
| 173 | 178 | assert_eq!( |