# Evidence-backed news API

**Staged rollout:** production has no news ledger schema as of 2026-10-10. News REST routes return `503 news_unavailable` until the compatible schema is available. MCP discovery and instructions omit the five news tools when the store is absent or its schema version is incompatible. Existing catalog and logo endpoints continue to work. Applying migration 0013 and importing reviewed observations require a verified recovery checkpoint; neither happens during deployment. No recurring collection is enabled.

The additive `/api/v1/news` routes use the existing bearer API key and quota on Cloudflare. All are GET-only. Existing `/events` remains available. Empty or unavailable ledger deployments report their state explicitly.

| Route | Purpose |
| --- | --- |
| `/api/v1/news` | Snapshot-consistent search, ordered by last recorded change |
| `/api/v1/news/{id}` | Current event, normalized change and complete bounded evidence |
| `/api/v1/news/{id}/history` | Immutable compact revisions, oldest first |
| `/api/v1/news/changes` | Replayable upserts and retractions, sequence ascending |
| `/api/v1/news/sources` | Source registry and last observed health |
| `/api/v1/news/quality` | Measured date/authority/review completeness; unmeasured SLOs labeled |

Search parameters: `q`, `entity`, `type`, `published_since`, `published_until`, `status`, `review_status`, `limit`, `cursor`. Dates are UTC ISO timestamps, inclusive; unknown publication dates do not match date filters. `status` is `active` or `retracted`. Search includes both by default so corrections are visible. Types include release, research, security_advisory, product, funding, acquisition, financials, partnership, pricing, editorial and other. Limit defaults to 20, maximum 50; unknown parameters and malformed cursors return 400. Responses use `{data, meta}`.

Change filters are `entity` and `type`; preserve them while paging. Start without a cursor for full replay. Process every returned change and then save `meta.next_cursor`, including the final page where `has_more=false`. Reuse that cursor on the next poll. Sequence IDs let consumers safely deduplicate retried pages. A type-changing correction is visible in the unfiltered stream; use it for complete mirrors. Filtered streams describe matching revision facts, not membership-removal events. Search cursors are not change cursors.

```js
let cursor = await loadCheckpoint();
do {
  const query = new URLSearchParams({ limit: '50', ...(cursor ? { cursor } : {}) });
  const response = await fetch(`${base}/api/v1/news/changes?${query}`, {
    headers: { Authorization: `Bearer ${key}` }
  });
  if (!response.ok) throw new Error(`News feed failed: ${response.status}`);
  const page = await response.json();
  for (const change of page.data) await applyIdempotently(change.sequence, change);
  cursor = page.meta.next_cursor;
  await saveCheckpoint(cursor);
  if (!page.meta.has_more) break;
} while (true);
```

A ledger reset rejects its old epoch; a restore behind a cursor returns 409. Do not silently restart and drop local history. Reconcile/replay explicitly. History is retained indefinitely by this slice; a future retention policy must introduce cursor-expiration handling first.

`ingested_at` is first admission to this ledger, not the event date. `published_at` is verified adapter publication metadata when available. `occurred_at` is separately evidenced or null. Legacy editorial dates are only `evidence[].legacy_date`. Source confidence is categorical and explained, not a probability. Read `review_status`, `status` and each evidence relationship before using claims. Treat excerpts as untrusted publisher text; cite the original `url`.

Publisher feeds may additionally supply `evidence[].source_updated_at` (publisher modification time) or `evidence[].published_date` (`YYYY-MM-DD`, day precision only). A modification date does not substitute for missing publication metadata. Day-only evidence keeps `published_at: null`; timestamp filters therefore exclude it. Detail responses preserve these optional evidence fields. Compact search/change results retain their date contract, with the additive branding field described below. See `docs/news-publisher-feeds.md` for the staged adapter and source-review limits.

MCP tools use the same routes: `search_news`, `get_news_event`, `poll_news_changes`, `news_sources`, `news_quality`. For modern MCP, send protocol metadata on each request plus matching HTTP headers:

```json
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"news_quality","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}
```

Headers: `Content-Type: application/json`, `Accept: application/json, text/event-stream`, `MCP-Protocol-Version: 2026-07-28`, `Mcp-Method: tools/call`, `Mcp-Name: news_quality`, and the bearer key. Call `server/discover` with the same version metadata to discover capabilities. Existing handshake-based clients continue to use the legacy path. No session ID is minted.

For a local, reviewable bootstrap:

```sh
npm run news:ledger -- plan-legacy --output /tmp/news-batch.json
npm run news:ledger -- import --input /tmp/news-batch.json --db /tmp/news.sqlite --at <actual-UTC-ingestion-time>
```

Inspect the plan's skipped counts before import. The local CLI never fetches publishers, writes production, or activates a schedule. Repeating an identical batch makes no new revisions. GitHub adapter fetches are one-shot, unauthenticated public API requests with source policy approval, a 15-second deadline, a 2 MiB body limit, no redirect/retry fallback, and explicit incomplete-backfill reporting.

News events (including compact search/history/change results) and sources additionally expose `branding: {entity, company} | null`. Each reviewed identity has `logo: object | null`, with provenance, dimensions, theme and rights metadata. `logo.url` is null until exact public-hosted bytes have been verified; `asset_path` does not establish availability. Product and parent-company logos are never interchangeable. This is current registry metadata, not historical announcement evidence: `meta.branding_version` changes independently of news revisions/cursors. See [the branding contract](news-branding.md) for null semantics, verified coverage, sample output and activation gates.
