# Top Year Feed History API

`/api/feed/history` replays an immutable Top Year publication exactly as the
dashboard archived it. Use this endpoint when the question is what the
published rolling Year feed looked like at a past time; do not reconstruct an
old ranking from current feed data.

## Endpoint

```text
GET /api/feed/history?sort=year&at=<UTC ISO-8601 timestamp ending in Z>
```

Parameters:

- `sort` is required and currently accepts only `year`.
- `at` is optional; an absent parameter defaults to the current time. When
  supplied it must be nonempty, have no surrounding whitespace, and use
  `YYYY-MM-DDTHH:mm:ssZ` with an optional one-to-three-digit fractional second
  before `Z`. Timestamps more than five minutes in the future are rejected.

The service selects the latest Year publication across the preserved legacy v1
ledger and the versioned D1 ledger that holds immutable v2/v3 publications,
using the newest durable manifest with `published_at <= at`, then
verifies that exact manifest and object. It does
not skip a corrupt or unavailable newest match to return an older publication.
Only artifacts actually published at the time qualify: a missed daily
publication remains a historical gap and is never minted retrospectively.
The selected publication's `asOfDay` can be earlier than the requested
timestamp because Top Year makes three same-as-of-day publication attempts from
the last completed UTC day and leaves a gap if all three fail.

Example:

```sh
curl 'https://shir-man.com/api/feed/history?sort=year&at=2026-08-15T12:00:00Z'
```

## Exact replay boundary

A successful response body is the archived R2 publication envelope verbatim:

- `schemaVersion`, `kind`, `publicationId`, and `algorithmVersion` identify the
  immutable artifact.
- `asOfDay`, `generatedAt`, `windowStart`, and `windowEnd` identify the
  publication and its rolling 365-complete-UTC-day target window.
- `coverage` records the actual retained daily coverage at publication time,
  including its materialized day count and completeness.
- `feed` is the exact published Year payload, including its per-source arrays,
  merged `popular` Top 100, Midjourney list, GitHub health, and virality map.
- `algorithmVersion` is the interpretation boundary. Historical
  `top-year-v1` bytes retain their original canonical-destination episode
  ranking and can include Hype in `popular`; they are never relabeled as a
  newer version.
- `top-year-v2` introduced the source-event ranking retained by current
  `top-year-v3`: distinct events are identified by `(feed source,
  source_id)`. Hacker News and Lobsters rank by latest stored points;
  LessWrong ranks by latest stored karma. Comments break only an exact-primary
  tie. Hype's provider units are incomparable, so its separate column uses a
  midpoint empirical percentile within each known provider cohort containing
  at least five eligible events.
- V2 and v3 `popular` are news-only: Hacker News, LessWrong, and Lobsters. They
  compare half-rank source percentiles, apply no source quota or repeated-source
  penalty, and remove exact URL/title duplicates. Hype stays in its own column.
- V2/v3 source-native values are the latest totals stored for events whose
  publication timestamp is inside the rolling window. They are not engagement
  gained during the window and are not event-day historical snapshots.
- Current v3 keeps the v2 identity, ranking, and scoring formula unchanged. Its
  additional publication boundary hydrates ambiguous legacy-event titles from
  the owning source rather than borrowing a title from another event that
  shares the canonical destination. If source-owned identity cannot be
  established safely, publication fails instead of emitting a mislabeled item.
  Every v3 source-event item records `event_title_verified: true` and a nonempty
  `title_provenance`; missing evidence makes the v3 payload invalid. Historical
  v2 bytes are preserved exactly and are not rewritten as v3.
- In every version, GitHub is empty and reports `status: "collecting"` until a
  direct 365-day star-gain pipeline exists. No Month, lifetime-star,
  repository-recency, or Trending filler is archived.
- `archivedAt` is the publication time used for historical selection and the
  `Last-Modified` response header.

The service reads the selected manifest, fetches its content-addressed object,
and verifies the stored SHA-256, byte size, schema version, publication id,
as-of day, and equality between the publication envelope and its D1 manifest.
It does not rebuild the ranking, merge later metadata, substitute the live Year
KV value, or fall back to Trending, Month, or another publication.

## Cache, CORS, and integrity behavior

Successful responses include:

- `Cache-Control: public, max-age=300, stale-while-revalidate=3600`
- a strong `ETag` derived from the archived object's stored SHA-256
- `Last-Modified` from the selected manifest's `published_at`
- `X-Year-History-Requested-At` with the normalized lookup timestamp
- `Link` entries for these docs and the OpenAPI document
- `Access-Control-Allow-Origin: *`
- `Access-Control-Expose-Headers: ETag, Last-Modified, Link, X-Year-History-Requested-At`

Send the strong ETag back in `If-None-Match` to receive `304 Not Modified` when
the same archived publication is still selected.

Common errors:

- `400 {"error":"invalid_sort"}` when `sort` is missing or not `year`
- `400 {"error":"invalid_at"}` when a supplied `at` is empty, has surrounding
  whitespace, does not match the exact UTC grammar above, or is more than five
  minutes in the future
- `404 {"error":"year_history_not_found"}` when no Year publication row
  existed at or before `at`
- `405 {"error":"method_not_allowed"}` with `Allow: GET` for an unsupported
  application method; the site's CORS `OPTIONS` preflight is handled earlier
  and returns `200` with `Access-Control-Allow-Methods: GET, OPTIONS` and
  `Cache-Control: no-store`
- `503 {"error":"year_history_storage_unavailable"}` when D1 or R2 is not bound
- `503 {"error":"year_history_archive_corrupt"}` when the selected
  manifest/object fails digest, byte-size, schema, or identity verification
- `503 {"error":"year_history_lookup_failed"}` for another D1/runtime failure

All failures are fail-closed. They never return a current, reconstructed, or
older substitute feed, and every `400`, `404`, `405`, or `503` response carries
`Cache-Control: no-store`.

## Retention and related resources

Top Year daily signals and publication objects have an application-level
`indefinite` retention policy. This endpoint exposes exact publications, not
the private compact v1 daily-signal archive. Those daily signals remain the
coverage/durability ledger; current v3 ranks the latest stored source-native
totals instead of deriving its score from the v1 canonical signals.

- OpenAPI: `/docs/api/top-year-history/openapi.json`
- Current Top Year: `/homepage/?sort=year`
- Current feed API: `/api/feed?sort=year`
- In-page agent: `/homepage/`
