koala73/worldmonitor · warning
CAP_VERIFICATION_FAILED
CAP_VERIFICATION_FAILED
Error message
saskalert: incomplete CAP verification; success clock unchanged
What it means
saskAlertBeforePublish throws this tagged error (code CAP_VERIFICATION_FAILED) when the fetched SK snapshot could not fully verify every alert against its CAP document — some CAP fetches failed or were skipped due to the time budget. The library refuses to advance the success clock (fetchedAt stays at the previous value), writes ERROR-state diagnostics plus any still-valid retained records from the previous snapshot, rebuilds the Canada alerts union, and only then throws so the publish pipeline records the source as degraded rather than fresh.
Solutions
- Check the logged diagnostics line (capVerificationFailed / capSkippedDeadline / reasons) to see which failure reason dominated and target that.
- If skippedDeadline dominates, raise CAP_PHASE_BUDGET_MS or CAP_CONCURRENCY in scripts/lib/saskalert.mjs so more CAP docs hydrate in time.
- If reasons show 'timeout' or 'http', treat as upstream degradation and let the next scheduled run retry; the previous snapshot is retained meanwhile.
- If reasons show 'invalid_link', verify the allowlist (SASKALERT_HOST) covers the CAP link hosts the feed now emits.
- Rely on the retention path: unverified-but-unexpired previous alerts remain published, so downstream consumers stay fed while verification is retried.
Example fix
// before
// caller treats any throw as total failure
class SaskAlertError extends Error { constructor(msg, code) { super(msg); this.code = code; } }
// after
try {
await publishSaskAlerts(data, { canonicalKey, ttlSeconds });
} catch (err) {
if (err?.code === 'CAP_VERIFICATION_FAILED') {
console.warn('SK alerts degraded; retained previous snapshot, will retry next cycle');
return; // do not page: success clock intentionally unchanged
}
throw err;
} Defensive patterns
Strategy: try-catch
Validate before calling
if (data?._capVerification && (data._capVerification.failed > 0 || data._capVerification.skippedDeadline > 0)) console.warn('SK snapshot will publish as degraded: ', data._capVerification); Try / catch
try {
await saskAlertBeforePublish(data, { canonicalKey, ttlSeconds });
} catch (err) {
if (err?.code === 'CAP_VERIFICATION_FAILED') {
// expected degraded path: previous snapshot retained, success clock unchanged
logger.warn({ key: canonicalKey, verification: data._capVerification }, 'SK alerts degraded');
return;
}
throw err;
} Prevention
- Always branch on err.code === 'CAP_VERIFICATION_FAILED' rather than message text.
- Size CAP_PHASE_BUDGET_MS and CAP_CONCURRENCY so a full feed hydrates with headroom.
- Alert on consecutive degraded runs — a single one is benign, a streak means upstream CAP endpoints are down.
- Keep ttlSeconds larger than the retry interval so retained ERROR-state snapshots do not expire before the next successful verification.
When it happens
Trigger: Any fetchSaskAlerts result whose _capVerification shows failed > 0 or skippedDeadline > 0 passed into saskAlertBeforePublish — i.e. one or more per-alert CAP document fetches returned HTTP errors, timed out, failed parsing/validation, or were skipped because the CAP_PHASE_BUDGET_MS deadline elapsed before hydration finished.
Common situations: Saskatchewan's CAP endpoints are slow or partially down during a large weather event; too many active alerts to hydrate within the time budget; transient network failures from the worker host; CAP links in the feed pointing at hosts not on the allowlist.
Related errors
- relay returned ${resp.status}
- no results
- relay returned ${resp.status}
- no results
- ${pagePath} is missing its recent-developments section
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/3cbd525645fc3e80.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/lib/saskalert.mjs:394
&& (!alert.expires || Date.parse(alert.expires) > nowMs)
&& Number.isFinite(alert.updatedAt ?? alert.publishedAt)
&& nowMs - (alert.updatedAt ?? alert.publishedAt) <= SASKALERT_MAX_CONTENT_AGE_MIN * 60_000)
: [];
const verifiedIds = new Set(data.alerts.map(alert => alert.id));
const snapshot = { alerts: [...data.alerts, ...retained.filter(alert => !verifiedIds.has(alert.id))] };
const contentAge = { ...saskAlertContentMeta(snapshot, nowMs), maxContentAgeMin: SASKALERT_MAX_CONTENT_AGE_MIN };
const metaPatch = { ...diagnostics, lastAttemptAt: nowMs, retainedRecords: retained.length };
// An incomplete attempt can remove ended records, but cannot advance the success clock.
await writeExtraKey(canonicalKey, snapshot, ttlSeconds, {
...previousMeta, fetchedAt, recordCount: snapshot.alerts.length,
sourceVersion: 'saskalert-v1', schemaVersion: 1, state: 'ERROR',
errorReason: 'CAP_VERIFICATION_FAILED', ...contentAge,
});
const written = await writeFreshnessMetadataSafely('alerts', 'saskalert', snapshot.alerts.length,
'saskalert-v1', ttlSeconds, fetchedAt, contentAge, metaPatch);
if (!written) throw new Error('saskalert: failed to persist verification diagnostics');
await rebuildCanadaAlertsUnion({ currentSource: { province: 'SK', snapshot, metaPatch: { ...metaPatch, fetchedAt } } });
throw Object.assign(new Error('saskalert: incomplete CAP verification; success clock unchanged'), { code: 'CAP_VERIFICATION_FAILED' });
}
export function saskAlertPublishTransform(data) {
return { alerts: Array.isArray(data?.alerts) ? data.alerts : [] };
}
export function saskAlertAfterPublish(data) {
const failed = Math.min(100, Math.max(0, Number(data?._capVerification?.failed) || 0));
const skippedDeadline = Math.min(100, Math.max(0, Number(data?._capVerification?.skippedDeadline) || 0));
if (failed > 0 || skippedDeadline > 0) {
return {
freshnessMetaPatch: {
sourceState: 'degraded',
errorCode: 'CAP_VERIFICATION_FAILED',
capVerificationFailed: failed,
capSkippedDeadline: skippedDeadline,
capFailureReasons: data._capVerification.reasons,
},View on GitHub (pinned to 7d06c8633d)