Six follow-ups from the link-archive audit, all the items with a
code-shaped fix. Behaviour verified per feature (stubbed + live where a
network or browser was needed); ARCHIVE.md status notes updated alongside.
- suggest: `archive.py suggest` + `make archive-suggest` print works
cited in data/*.bib (url wins, DOI-only resolves to doi.org/...) but
absent from the manifest, as manifest-ready lines. Read-only, offline.
- aliases: optional `aliases:` manifest field for equivalent URLs no
normalisation can derive (DOI vs. landing URL). Enforced like canonical
URLs on both sides (archive.py pre-scan + Archive.hs validator);
ArchiveIndex drops alias keys matching a takedown. FIPS 203 now carries
its DOI form, so the simd paper's DOI citation resolves.
- check scheduling: systemd user timer (systemd/archive-check.{service,
timer}, symlink-installed) runs the rot scan daily. cmd_check gains an
offline canary guard so an unattended scan on an offline machine leaves
state untouched instead of mass-flipping entries to rotted.
- Wayback fallback: an original already dead at first fetch falls back to
its most recent existing Wayback capture (raw id_ bytes through the
normal pipeline), honouring a preserved X-Archive-Orig noarchive and
recording `fetched-from`. Dead-only, never during refresh.
- bibliography annotation: Filters.Archive exports annotateBlock;
Citations.hs applies it to each CSL-rendered entry, so bibliography
links get the same affordance / rotted-flip as body links.
- search-UI filter: archive "exclude/only" + "link status" filters on the
search page, backed by a new data/archive-meta.json. Scoped apart from
the epistemic `status` filter (own state, classes, labels) rather than
renamed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Preserve external works the site cites against link rot, host them at
permanent /archive/<slug>/ URLs in site chrome, and treat them as
first-class citizens of the backlinks and similar-pages indexes.
Curated, not crawled: the author adds one line to archive/manifest.yaml
and the build fetches, hashes, snapshots, and indexes the work.
* archive/manifest.yaml + tools/archive.py (fetch / refresh / wayback /
check / gc) — PDFs downloaded directly, HTML pages snapshotted with a
vendored monolith (tools/bin/monolith @ 2.10.1) into a single
self-contained file with the archive CSP and a noarchive robots meta
injected. Per-entry PROVENANCE.json committed; gitignored .txt
sidecars regenerated from the artifact's SHA-256.
* build/Archive.hs + build/ArchiveIndex.hs + build/Filters/Archive.hs
— Hakyll rules for /archive/ and /archive/<slug>/, a body Pandoc
filter that appends an archive affordance to live citations and
flips dead ones to the local copy on archive.py check's asymmetric
hysteresis (rotted needs 3 fails over >= 14 days; one ok recovers).
* build/Backlinks.hs — keeps archived external URLs through pass 1 and
canonicalises them to /archive/<slug>/ in pass 2, producing a
"Referenced by" section grouped by the fragment each citation
targets. build/Stats.hs gains a "Link archive" telemetry block on
/build/ (count, total size, median age, by-status / by-quality /
by-visibility, orphans).
* Integrity: archive.py fetch and build/Archive.hs (via sha256sum)
both re-hash every committed artifact, so a tampered file halts the
build even with cabal invoked directly or no .venv present. refresh
refuses to replace an uncommitted prior snapshot and rolls back
atomically on any exit path. removed.yaml is honoured by fetch,
wayback, and check using canonical-form (tracking-stripped,
arXiv-canonicalised) comparison.
* visibility: private keeps an entry in-repo but undeployed.
nginx/archive.conf emits X-Robots-Tag: noindex, noarchive for raw
artifacts that cannot carry meta directives.
The full design, phase plan (1-5), and three refinement passes live
in ARCHIVE.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>