Skip to content

docs(plans): health-readiness probe — content-age tracking#3594

Merged
koala73 merged 1 commit into
mainfrom
plan/health-readiness-content-age-probe
May 4, 2026
Merged

docs(plans): health-readiness probe — content-age tracking#3594
koala73 merged 1 commit into
mainfrom
plan/health-readiness-content-age-probe

Conversation

@koala73

@koala73 koala73 commented May 4, 2026

Copy link
Copy Markdown
Owner

Plan doc capturing the architectural follow-up identified during the 2026-05-04 disease-outbreaks incident.

Problem

/api/health reports seeder-RUN freshness, not CONTENT freshness. For sparse upstream sources (WHO Disease Outbreak News, IEA OPEC monthly reports, central-bank releases, WB annual indicators) these diverge — seeder runs fine, seed-meta fetchedAt stays fresh, but the freshest item the user actually sees in the cache is days or weeks old. Health says OK; UI renders nothing.

Proposal

Opt-in content-age contract:

  • Seeder declares itemTimestamp(item) extractor + maxContentAgeMin budget
  • runSeed writes newestItemAt/oldestItemAt/maxContentAgeMin into seed-meta when declared
  • api/health adds STALE_CONTENT status that fires when content is older than the seeder's content-age budget
  • Backwards compatible — legacy seeders without itemTimestamp keep current behavior

Sprints

Sprint Scope
1 Infra: runSeed + health classifier (~150 LOC + tests)
2 Pilot: disease-outbreaks (single-file change)
3 Migrate sparse seeders (climate news, IEA, news-digest)
4 Migrate annual-data seeders (WB resilience indicators — formalizes #3582's canonical-mirror)
5 Optional: fast-cadence seeders for completeness

Each sprint is an independent PR; staged rollout reduces risk.

Companion incidents this would catch

The pattern: fetched-recently is not the same as fresh-content. This plan makes the distinction first-class.

Plan doc lives at docs/plans/2026-05-04-001-feat-health-readiness-probe-content-age-plan.md (force-added since docs/plans/ is gitignored — matches the convention for past plans #2890, #3402).

Captures the architectural follow-up identified during the 2026-05-04
disease-outbreaks incident: /api/health currently reports seeder-run
freshness, not content freshness. For sparse upstream sources
(WHO Disease Outbreak News, IEA OPEC reports, central-bank releases,
WB annual indicators) these diverge — seeder runs fine, seed-meta
fetchedAt stays fresh, but the freshest item the user sees is days or
weeks old. Health says OK; UI renders nothing.

Plan opts seeders into a parallel content-age contract:
- runSeed accepts itemTimestamp / itemsPath / maxContentAgeMin
- seed-meta carries newestItemAt/oldestItemAt/maxContentAgeMin when set
- api/health reports new STALE_CONTENT status when content is older than
  the seeder's content-age budget

Backwards compatible — legacy seeders without itemTimestamp keep current
behavior. Pilot on disease-outbreaks (today's incident's origin), then
migrate sparse + annual seeders over Sprints 3-4.

Companion to PRs #3582 (canonical-envelope-mirror), #3593 (disease-
outbreaks TGH + time-filter fixes), and the broader 'fetched-recently is
not the same as fresh-content' insight.
@mintlify

mintlify Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
WorldMonitor 🟢 Ready View Preview May 4, 2026, 5:28 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@vercel

vercel Bot commented May 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment May 4, 2026 5:27pm

Request Review

@greptile-apps

greptile-apps Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a plan document for a new STALE_CONTENT health status that distinguishes seeder-run freshness from content freshness, addressing a production incident where disease-outbreaks showed OK despite all cached items being 11+ days old. The plan is architecturally sound and well-staged across 5 sprints, but the classifyHealth pseudocode contains a concrete implementation trap: it reads meta.newestItemAt from the return value of readSeedMeta(), which does not carry that field — causing the STALE_CONTENT check to silently never fire unless readSeedMeta is explicitly extended. The plan also omits adding STALE_CONTENT to STATUS_COUNTS, which would leave its alerting tier implicitly defined by the ?? 'warn' fallback.

Confidence Score: 4/5

Safe to merge as a plan doc; the P1 is a spec-level gap that must be caught in the Sprint 1 implementation PR, not a runtime defect today.

Docs-only PR with no production code changes. The P1 (readSeedMeta not extended, STALE_CONTENT would silently never fire) is a real implementation trap if Sprint 1 follows the pseudocode literally, but it affects only the future implementation PR, not this merge. P2s are minor naming and omission issues. Score is 4 rather than 5 because the P1 spec gap in a plan doc should be corrected before Sprint 1 begins.

docs/plans/2026-05-04-001-feat-health-readiness-probe-content-age-plan.md — the classifyHealth pseudocode section and the STATUS_COUNTS omission need correction before Sprint 1 kicks off.

Important Files Changed

Filename Overview
docs/plans/2026-05-04-001-feat-health-readiness-probe-content-age-plan.md New plan doc for content-age health tracking; contains a silent-failure risk in the classifyHealth pseudocode where meta.newestItemAt is read from the wrong object, plus a missing STATUS_COUNTS entry and one incorrect function name reference.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[classifyKey called] --> B{Redis error or\nmeta read failed?}
    B -- Yes --> C[REDIS_PARTIAL]
    B -- No --> D{hasData?}
    D -- No --> E{cascadeCovered?}
    E -- Yes --> F[OK_CASCADE]
    E -- No --> G{EMPTY_DATA_OK?}
    G -- Yes --> H{seedStale?}
    H -- Yes --> I[STALE_SEED]
    H -- No --> J[OK]
    G -- No --> K{isOnDemand?}
    K -- Yes --> L[EMPTY_ON_DEMAND]
    K -- No --> M[EMPTY]
    D -- Yes --> N{records == 0?}
    N -- Yes --> O{EMPTY_DATA_OK / onDemand?}
    O --> P[EMPTY_DATA / EMPTY_ON_DEMAND]
    N -- No --> Q{seedError?}
    Q -- Yes --> R[SEED_ERROR]
    Q -- No --> S{seedStale?}
    S -- Yes --> I
    S -- No --> T{minRecordCount\nviolated?}
    T -- Yes --> U[COVERAGE_PARTIAL]
    T -- No --> V{NEW: newestItemAt\nand maxContentAgeMin\nin seed-meta?}
    V -- No --> J
    V -- Yes --> W{contentAgeMin >\nmaxContentAgeMin?}
    W -- Yes --> X[STALE_CONTENT]
    W -- No --> J
Loading

Reviews (1): Last reviewed commit: "docs(plans): health-readiness probe — co..." | Re-trigger Greptile

Comment on lines +144 to +158

Add the `STALE_CONTENT` status grade. Today `api/health.js:checkSeedKey` reads `seedMeta` and reports `OK` / `STALE_SEED` / `EMPTY_DATA` based on `seedAgeMin` and `recordCount`. The new logic:

```js
function classifyHealth(meta, freshnessConfig) {
const now = Date.now();
const seedAgeMin = (now - meta.fetchedAt) / 60000;
if (meta.recordCount === 0 && !freshnessConfig.allowEmpty) return 'EMPTY_DATA';
if (seedAgeMin > freshnessConfig.maxStaleMin) return 'STALE_SEED';

// NEW — only checked when the seeder opted in
if (typeof meta.newestItemAt === 'number' && typeof meta.maxContentAgeMin === 'number') {
const contentAgeMin = (now - meta.newestItemAt) / 60000;
if (contentAgeMin > meta.maxContentAgeMin) return 'STALE_CONTENT';
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 readSeedMeta not extended — STALE_CONTENT would silently never fire

The classifyHealth pseudocode reads meta.newestItemAt and meta.maxContentAgeMin directly, but in api/health.js the variable meta inside classifyKey is the return value of readSeedMeta() — an object shaped { seedAge, seedStale, seedError, metaReadFailed, metaCount }. Accessing .newestItemAt on that return object yields undefined, so the guard typeof meta.newestItemAt === 'number' is always false and STALE_CONTENT never fires.

Sprint 1 must explicitly extend readSeedMeta to also pull newestItemAt and maxContentAgeMin from the parsed Redis payload (the raw object is at unwrapEnvelope(parseRedisValue(keyMetaValues.get(seedCfg.key))).data) and return them alongside the existing fields, before the check in classifyKey can work.

Comment on lines +163 to +168
`STALE_CONTENT` is structurally distinct from `STALE_SEED`. Operator playbooks differ:

- `STALE_SEED` → seeder is broken; check Railway logs.
- `STALE_CONTENT` → upstream isn't publishing (or our parser is dropping recent items); check the source.

Health response surfaces both ages when the seeder opted in:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 STALE_CONTENT missing from STATUS_COUNTS

The plan does not include adding STALE_CONTENT to the STATUS_COUNTS map in api/health.js (line 651). If omitted, the ?? 'warn' fallthrough on line 738 silently handles it — but it also means the status doesn't show up in the ok/warn/crit bucket docs, is invisible to anyone reading the map, and any future change to that fallback would silently alter alert severity. Sprint 1 should explicitly add STALE_CONTENT: 'warn' (or whatever tier is decided in Open Question 1) to STATUS_COUNTS.

Comment on lines +127 to +130
}
if (newest !== -Infinity) {
contentMeta = {
newestItemAt: newest,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Wrong function name — classifyKey, not checkSeedKey

The Sprint 1 architecture section refers to api/health.js:checkSeedKey as the entry point for the new logic. The actual function in the current file is classifyKey (line 597). The counterpart that reads seed-meta is readSeedMeta (line 556). Naming the right functions in the plan reduces the risk of Sprint 1 touching the wrong call sites.

@koala73
koala73 merged commit f1c48c7 into main May 4, 2026
13 checks passed
koala73 added a commit that referenced this pull request May 4, 2026
…revisions (#3595)

Five rounds of Codex review against plan #3594:

- Round 1 (8 findings): pilot threshold won't catch incident; canonical-mirror loses content fields; synthetic timestamps mask staleness; all-undated falls through to OK; wrong health.js target symbols; soft-disabled budget; brittle autodetect; missing tests. Adopted contentMeta(data) -> {newestItemAt, oldestItemAt} API.
- Round 2 (3 findings): envelope-writer chain incomplete (need _seed-envelope-source.mjs + parity mirrors + _seed-contract.mjs); classifier precedence wrong; disease snippet broke isNaN filters and mapItem.
- Round 3 (3 findings): TGH source missed migration; stale classifier code block contradicted Sprint 1; helpers leaked via list-disease-outbreaks + bootstrap.
- Round 4 (3 findings): replacement classifier still used bare 'return' but real code uses status='X' assignment; mapItem section contradicted strip contract; grep missed _originalPublishedMs.
- Round 5 (1 finding): test descriptions still said cached items 'have helper fields'; rewrote to separate pre-publish in-memory layer from post-strip published-canonical layer.

Net: ~280 prod LOC + ~250 test LOC for Sprint 1; explicit envelope-writer chain coverage; Sprint 2 disease pilot covers all 3 sources (WHO/RSS/TGH) + helper-field strip via publishTransform + anti-regression tests.

Companion: PR #3593 (immediate disease-outbreaks fixes), #3582 (canonical-envelope-mirror).
fuleinist pushed a commit to fuleinist/worldmonitor that referenced this pull request May 9, 2026
…a73#3594)

Captures the architectural follow-up identified during the 2026-05-04
disease-outbreaks incident: /api/health currently reports seeder-run
freshness, not content freshness. For sparse upstream sources
(WHO Disease Outbreak News, IEA OPEC reports, central-bank releases,
WB annual indicators) these diverge — seeder runs fine, seed-meta
fetchedAt stays fresh, but the freshest item the user sees is days or
weeks old. Health says OK; UI renders nothing.

Plan opts seeders into a parallel content-age contract:
- runSeed accepts itemTimestamp / itemsPath / maxContentAgeMin
- seed-meta carries newestItemAt/oldestItemAt/maxContentAgeMin when set
- api/health reports new STALE_CONTENT status when content is older than
  the seeder's content-age budget

Backwards compatible — legacy seeders without itemTimestamp keep current
behavior. Pilot on disease-outbreaks (today's incident's origin), then
migrate sparse + annual seeders over Sprints 3-4.

Companion to PRs koala73#3582 (canonical-envelope-mirror), koala73#3593 (disease-
outbreaks TGH + time-filter fixes), and the broader 'fetched-recently is
not the same as fresh-content' insight.
fuleinist pushed a commit to fuleinist/worldmonitor that referenced this pull request May 9, 2026
…revisions (koala73#3595)

Five rounds of Codex review against plan koala73#3594:

- Round 1 (8 findings): pilot threshold won't catch incident; canonical-mirror loses content fields; synthetic timestamps mask staleness; all-undated falls through to OK; wrong health.js target symbols; soft-disabled budget; brittle autodetect; missing tests. Adopted contentMeta(data) -> {newestItemAt, oldestItemAt} API.
- Round 2 (3 findings): envelope-writer chain incomplete (need _seed-envelope-source.mjs + parity mirrors + _seed-contract.mjs); classifier precedence wrong; disease snippet broke isNaN filters and mapItem.
- Round 3 (3 findings): TGH source missed migration; stale classifier code block contradicted Sprint 1; helpers leaked via list-disease-outbreaks + bootstrap.
- Round 4 (3 findings): replacement classifier still used bare 'return' but real code uses status='X' assignment; mapItem section contradicted strip contract; grep missed _originalPublishedMs.
- Round 5 (1 finding): test descriptions still said cached items 'have helper fields'; rewrote to separate pre-publish in-memory layer from post-strip published-canonical layer.

Net: ~280 prod LOC + ~250 test LOC for Sprint 1; explicit envelope-writer chain coverage; Sprint 2 disease pilot covers all 3 sources (WHO/RSS/TGH) + helper-field strip via publishTransform + anti-regression tests.

Companion: PR koala73#3593 (immediate disease-outbreaks fixes), koala73#3582 (canonical-envelope-mirror).
@koala73
koala73 deleted the plan/health-readiness-content-age-probe branch June 25, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant