Skip to main content

max / makenotwork

Curate the demo media and widen the catalog to five items a project The manifest built last commit was a checklist with 19 empty rows. It is now 34 filled ones, and the catalog behind it is big enough that a storefront grid reads as a grid. Catalog. Eleven items became twenty-five, five per project, still covering every ItemType. Three cards in a five-wide grid read as an empty shop whatever the cover art looks like, so widening had to come first: curating 19 slots and then discovering the roster wanted 34 would have wasted half the pass. Curation. Thirty images from the Met's Open Access collection and four public-domain recordings from Wikimedia Commons. Both were picked over a general image search because each exposes the licence as a queryable field, so the claim this repo makes on a public box is the institution's own rather than a human's reading of a page. Period imagery throughout, themed per storefront: instruments for a reissue label, ornament plates for a tools suite, printing history for a press, 19th-century photographs for a CC0 field library. Those photographs are the works themselves, not just covers. Pins are facts. Every digest is of bytes that were actually fetched, then verified again from a cold cache by the Rust client rather than the Python that wrote them. That second pass is what caught the interesting part: the Met re-encodes. Twelve of the thirty changed digest within half an hour of first being pinned, same object, a few kilobytes different. The manifest header now says so and gives the recovery, because a mismatch here is going to happen and reading it as an attack would waste an afternoon. Mismatch still fails closed; a warning would turn "this file was replaced" into a log line nobody reads, on a box whose whole job is being looked at. Two sealed baselines move with the content: visible items 9 -> 20, and placeholder covers 5 -> 0. Zero is now the true value, so a placeholder appearing at all means the manifest or storage stopped working. Frame 1 of the carousel is 3:2 rather than 16:10, to hold the storefront header and the first row of items together. The caption claims a shop; the title alone could be any page, and the grid alone loses whose shop it is.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-07 17:12 UTC
Signed with PGP, not checked
Commit: d34730550fc228f95023674a85f0be08d71f27fc
Parent: 87877f7
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!(