Changelog d98eefa
Forget archive records of sources no longer cited, and count only cited ones
source-archive.json only ever grew: when a source was removed or rewritten, its record stayed. 406 of 4,124 addresses were no longer cited by anything, and the "N sources have a copy in the Internet Archive" figure on the home, About and Data pages counted them (326 copies). - scripts/archive-sources.cjs prune forgets uncited addresses (no network); check, save and probe prune whenever they write, and status reports how many are waiting. A copy recorded under an older spelling of an address still cited (http/https, www, trailing slash) moves to it when it has none: one did (eyeradio.org). - Pruned: 4,124 -> 3,719 records; no remaining record changed. - The archived count now comes from citations.js over the cited pages only, the same count /sources/ already used, so every page shows the same figure (3,590) and a stale record cannot inflate it again. Claude-Session: https://claude.ai/code/session_015PFhJhHBpXsnUocs8vvYAd
9 files of code, design or docs +93 −1,677 lines
Code, design and documents 9 files
docs/DATA-MAINTENANCE.md +1 −0
@@ -1001,4 +1001,5 @@ script. The file is written by a script, never by hand: ``` node scripts/archive-sources.cjs status # how many sources have a recorded copy +node scripts/archive-sources.cjs prune # forget addresses no source cites any more (no network) node scripts/archive-sources.cjs check 200 # record the copies the Archive already holds node scripts/archive-sources.cjs save 20 # ask the Archive to save what it does not hold
scripts/archive-sources.cjs +34 −2
@@ -1,3 +1,3 @@ -// Archived copies of the sources: node scripts/archive-sources.cjs [status|check|save|probe] [limit] [--random] [--include-dead] +// Archived copies of the sources: node scripts/archive-sources.cjs [status|check|save|probe|prune] [limit] [--random] [--include-dead] // Run from the project root. Needs js-yaml (already in node_modules) and Node 18+ (fetch). // @@ -7,4 +7,8 @@ // probe open the sources that have no copy yet, as a browser would, and record the ones whose // site refuses or has lost them (see "dead" below) +// prune forget the addresses no source cites any more (no network); check, save and probe do +// this too whenever they write. A copy recorded under an older spelling of an address +// that is still cited (http or https, with or without www, a trailing slash) moves to +// that address when it has no copy of its own. // // `limit` caps how many addresses one run handles (default 50), so a run stays short and polite; @@ -82,6 +86,27 @@ const markDead = (url, reason, status) => { }; +// Addresses in the file that no source cites any more: a source removed or rewritten. They would +// otherwise stay forever and count as archived sources they no longer are. +const spelling = (u) => { try { u = decodeURI(u); } catch {} return u.replace(/^https?:\/\/(www\.)?/i, "").replace(/\/$/, "").toLowerCase(); }; +const uncited = () => Object.keys(archive).filter((u) => !urls.has(u)); +const prune = () => { + const cited = new Map(all.map((u) => [spelling(u), u])); + let moved = 0; + const gone = uncited(); + for (const u of gone) { + const now = cited.get(spelling(u)); + if (now && archive[u].archived && !archive[now]?.archived) { + const { dead, ...copy } = archive[u]; + archive[now] = copy; + moved++; + } + delete archive[u]; + } + return { removed: gone.length, moved }; +}; + // written to a temporary file first and then renamed, so a full disk or a crash cannot leave half a file const write = () => { + prune(); fs.writeFileSync(FILE + ".tmp", JSON.stringify(Object.fromEntries(Object.entries(archive).sort(([a], [b]) => a.localeCompare(b))), null, 1) + "\n"); fs.renameSync(FILE + ".tmp", FILE); @@ -155,8 +180,15 @@ async function saveLoggedIn(url) { } +if (uncited().length) console.log(`no longer cited: ${uncited().length} addresses in ${FILE} (\`prune\` forgets them)`); + (async () => { if (what === "status") return; + if (what === "prune") { + const { removed, moved } = prune(); + write(); + return console.log(`prune: forgot ${removed} addresses no source cites${moved ? `, ${moved} copies moved to the address now cited` : ""}`); + } if (what === "save") console.log(KEYS ? "save: logged in" : "save: anonymous (no IA_ACCESS_KEY / IA_SECRET_KEY in .env)"); - if (!["check", "save", "probe"].includes(what)) return console.log("unknown command: use status, check, save or probe"); + if (!["check", "save", "probe"].includes(what)) return console.log("unknown command: use status, check, save, probe or prune"); if (what === "probe") return probe(); const pending = all.filter(what === "check" ? stale : (u) => !archive[u]?.archived && !resting(u));
source-archive.json +43 −1667
Not shown: a picture, a generated file, or past the length shown per change.
src/components/ProjectNumbers.astro +1 −1
@@ -5,5 +5,5 @@ import { SEGMENTS, DATA_SNAPSHOT, places, countries, changes, articles, subunits import { MAP_VIEWS } from "../lib/map-views.js"; import { cultureIn } from "../lib/culture.js"; -import { archivedCount } from "../lib/archive.js"; +import { archivedCount } from "../lib/citations.js"; import { sourceKey } from "../lib/sources.js";
src/lib/archive.js +1 −1
@@ -6,3 +6,3 @@ import archive from "../../source-archive.json"; export const archivedUrl = (url) => archive[url]?.archived ?? null; -export const archivedCount = Object.values(archive).filter((x) => x.archived).length; +// how many of the cited sources have a copy: archivedCount in citations.js
src/lib/citations.js +8 −0
@@ -8,4 +8,5 @@ import { getCollection } from "astro:content"; import { places, changes, SEGMENTS, CONTEXT } from "./countries.js"; import { cultureEntries, cultureSection } from "./culture.js"; +import { archivedUrl } from "./archive.js"; const placeOf = new Map(places.map((c) => [c.slug, c])); @@ -49,2 +50,9 @@ export const isBook = (s) => s.type === "book" || s.type === "chapter" || Boolea export const bookSlug = (t) => t.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80); export const domainOf = (url) => new URL(url).hostname.replace(/^www\./, ""); + +// Every web page cited, once, and how many have a copy in the Internet Archive (an archive.org page +// counts as one already). Only cited pages count: an address left in source-archive.json for a +// source that is no longer cited adds nothing (scripts/archive-sources.cjs prune forgets those). +export const citedUrls = new Set(cites.filter((x) => x.s.url).map((x) => x.s.url)); +export const isArchived = (u) => Boolean(archivedUrl(u)) || /^https?:\/\/([a-z0-9-]+\.)?archive\.org\//i.test(u); +export const archivedCount = [...citedUrls].filter(isArchived).length;
src/pages/about.astro +1 −1
@@ -8,5 +8,5 @@ import { MAP_VIEWS } from "../lib/map-views.js"; import { cultureIn } from "../lib/culture.js"; import { getFacts } from "../lib/facts.js"; -import { archivedCount } from "../lib/archive.js"; +import { archivedCount } from "../lib/citations.js"; import { sourceKey } from "../lib/sources.js"; import { survey } from "../lib/survey.js";
src/pages/data/index.astro +1 −1
@@ -4,5 +4,5 @@ import Base from "../../layouts/Base.astro"; import { scoreRows, historyRows, priceRows, panelRows, PANEL_FROM } from "../../lib/exports.js"; import { lastUpdated, formatDate, DATA_SNAPSHOT, ESTIMATE_WINDOW } from "../../lib/countries.js"; -import { archivedCount } from "../../lib/archive.js"; +import { archivedCount } from "../../lib/citations.js"; import { SHOW_PRICES } from "../../lib/config.js";
src/pages/sources/index.astro +3 −4
@@ -6,6 +6,5 @@ import Base from "../../layouts/Base.astro"; import { places } from "../../lib/countries.js"; -import { archivedUrl } from "../../lib/archive.js"; -import { cites, isBook, bookSlug, domainOf } from "../../lib/citations.js"; +import { cites, citedUrls, isArchived, isBook, bookSlug, domainOf } from "../../lib/citations.js"; import { Image } from "astro:assets"; import { DATASETS } from "../../lib/datasets.js"; @@ -66,10 +65,10 @@ const localRows = [...local] // Site icons come from DuckDuckGo's icon service and are not stored here; a missing one is hidden. const icon = (d) => `https://icons.duckduckgo.com/ip3/${d}.ico`; -const urls = new Set(cites.filter((x) => x.s.url).map((x) => x.s.url)); +const urls = citedUrls; const urlCount = urls.size; // pages with a copy recorded in the Internet Archive (see source-archive.json), counting a source // cited by its Archive address already; scripts/archive-sources.cjs status counts the same pages // an Internet Archive page (the Wayback Machine or an archive.org item) counts as archived already -const archivedOf = (set) => [...set].filter((u) => archivedUrl(u) || /^https?:\/\/([a-z0-9-]+\.)?archive\.org\//i.test(u)).length; +const archivedOf = (set) => [...set].filter(isArchived).length; const archived = archivedOf(urls); const pct = Math.round((archived / urlCount) * 100);