methodology

How this archive decides what it says. Every rule below is the one the code runs: a status is derived, a link is classified, a closure is dated, and a photograph is judged by who held the camera. Where a rule gets sharp, it is spelled out here instead of left for you to infer.

how a status is decided

A specimen’s archive status is not typed in. deriveArchiveStatus() in src/data/types.ts computes it from the record, and the first rule that matches wins:

closed
the spot is closed. This is checked before anything else, so every specimen of a closed business reads closed however it was selling.
retired
the price note contains “sold out”, case-insensitively, or the specimen was sold only at one event (eventOnly), which is over by the time the archive files it. Either outranks the confidence tier and the link sweep.
unverified
the confidence tier is 4. A verified photograph does not override this.
seen_once
the confidence tier is 3 and the image is not verified.
lost
the specimen has a buyUrl and the last sweep found it gone. Only a specimen that would otherwise be current reaches this rule, so a lost listing is a current item whose own product page disappeared, whatever the shirt is doing in the store.
current
none of the above matched.

confidence and sources

Every specimen carries a confidence tier from 1 to 4. The derivation reads only two things from it: 4 makes a specimen unverified even when a photograph exists, and 3 makes it seen_once when no photograph is verified. Tiers 1 and 2 are treated identically, so the difference between them is not visible in a status.

source records how a sighting was reported, not who took the photograph. A spot’s source is one of shopify, instagram, web, crowdsource, in_person or UberEats. A specimen’s source is free text naming the page or platform it came from: an official store, an Instagram post, a delivery-app listing, a walk-in, a reader’s submission.

lastSeen on a specimen is the day it was last sighted. lastVerified on a spot is the day a person last checked it. A sweep never writes either.

tiers 1 and 2
the object was seen on the business’s own storefront, listing or shelf, with a price.
tier 3
reported once and not yet confirmed with a photograph.
tier 4
unconfirmed, and worth checking in person.

the link sweep

bun run sweep asks every product page (buyUrl) and every storefront (onlineStore) whether it still answers, and writes the answer into the catalog as linkStatus and linkCheckedAt on the specimen, storeStatus and storeCheckedAt on the spot.

Instagram and UberEats hosts are not checked: they reject non-browser clients, so a sweep there would only ever say blocked.

A gone product page turns a current specimen into a lost listing, and the buy link falls back to the storefront. A gone storefront on an open spot is listed on /wanted for someone to go and look at; it does not close the spot or change a specimen’s status.

A sweep is evidence, not verification. It never touches lastSeen or lastVerified, which only a person sets.

The sweep runs on the 15th of each month, records its readings, runs the full check and opens a pull request. The lychee job is different on purpose: it fails a build on a dead link, this one records it.

live
the page answered 2xx or 3xx and is not a parked domain.
gone
the page answered 404 or 410, its hostname no longer resolves, or the domain is parked and serving an advert for the name. Nothing else counts.
blocked
anything ambiguous: a WAF 403, a rate limit, a 5xx, a timeout, a reset connection. blocked changes nothing; it is not evidence that a page is dead.

the sighting logs

The catalog holds only the latest state. Two logs hold what it held before, and neither is ever regenerated:

bun run observe compares the catalog with each log’s last entry and appends what differs. It never rewrites, reorders or removes an entry. A record that has left the catalog gets one last reading marked removed, dated the day the absence was noticed.

A reading is dated by when the thing was seen, not when the script ran. A specimen is dated by lastSeen, except a lost listing, which is dated by the later of lastSeen and the linkCheckedAt that found the page gone. A spot’s closure is dated by closedOn when a source gives it, otherwise by lastVerified; a storefront gone dark is dated by the later of lastVerified and storeCheckedAt.

/log renders both logs as one ledger, /wrapped sums a year from them, and sightings.json in the open-data export carries both. The data audit warns when the logs are behind the catalog.

observations.ts
one dated reading per specimen each time its price, price note, confidence, photo status or derived status moves.
spot-observations.ts
one dated reading per spot each time it closes, moves, changes or loses its storefront, or leaves the archive.

closures

A closed spot carries closed: true, a closedNote, and closedOn in YYYY-MM-DD only where a source states the date. The audit rejects a closedOn that is malformed or on a spot that is not closed.

The spot log dates the closure by closedOn, falling back to lastVerified; /closed sorts by the same, and /wrapped counts only closures dated in the current year.

A closure is listed only if the archive recorded it. It doesn’t reconstruct places it never saw, and a business is never added because it closed.

community areas

Neighborhoods are colloquial — Bucktown, Wrigleyville — and stay that way on a spot. The city’s unit is the community area, and every spot is filed into one of the 77 by point-in-polygon against boundaries fetched from the Chicago Data Portal.

bun run areas:assign writes src/data/spot-areas.generated.ts; the data audit fails when a spot has no entry, so adding a spot means running it once. A neighborhood that straddles two areas is filed by where each spot actually stands, not by a judgment call.

Coverage on /wanted and in the open-data export is reported in community areas and the nine sides that group them. A neighborhood’s chapter is filed under the side most of its spots fall on; a tie goes to the side the city lists first.

the camera policy

imageCredit decides whether a photograph can be republished, and it is set per photograph, not per directory.

source is not the photographer: it records how the sighting was reported. Two items marked in_person can still use the business’s own product photography.

A specimen counts as photographed when imageStatus is verified, and the audit requires every verified image to have an HTTPS imageSourceUrl. A delivery-app image gap is marked blocked, not not_found, because the app will not serve the file to the archive.

A removal request doesn’t get argued: /correct files it as an issue labelled correction and removal.

absent
the photograph belongs to the business or platform at imageSourceUrl. It is borrowed with attribution and a link back, is not republishable, and comes down if the business asks.
set
it names whoever held the camera — the archive itself, or a submitter who granted it. That image is the archive’s to publish, to keep when a storefront goes down, and to keep when a business changes its mind.

the open-data export

bun run data:generate, which runs inside bun run build, writes spots.json, specimens.csv, sightings.json and a LICENSE.txt that states the terms in words.

The catalog and the logs are licensed CC BY 4.0. Credit it as “chirts, Chicago merch archive (chirts.kanav.net)” and link to the site where a link is possible.

The photographs are not part of the export. It carries imageSourceUrl and imageCredit, never the file. submittedBy and imageCredit name people only where they asked to be credited.

The files are rebuilt on every deploy and served with open CORS headers. /data describes the fields and the caveats.

where these rules live

Each rule above is implemented in the files below. Change the code and this page is wrong, so the paths are here to check against:

src/data/types.ts
status derivation, labels, tiers, sources and every field named above.
scripts/sweep-lib.ts
what counts as live, gone, blocked and parked.
scripts/observations-lib.ts
what a reading is, how it is dated, and what is appended.
scripts/assign-community-areas.ts
the point-in-polygon assignment and the generated area map.
scripts/generate-data-export.ts
the export files and the licence text.
src/lib/photos.ts
what counts as a photograph and as the archive’s own.