{"record":{"id":"3cbd525645fc3e80","repo":"koala73/worldmonitor","slug":"cap-verification-failed","errorCode":"CAP_VERIFICATION_FAILED","errorMessage":"saskalert: incomplete CAP verification; success clock unchanged","messagePattern":"saskalert: incomplete CAP verification; success clock unchanged","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"scripts/lib/saskalert.mjs","lineNumber":394,"sourceCode":"      && (!alert.expires || Date.parse(alert.expires) > nowMs)\n      && Number.isFinite(alert.updatedAt ?? alert.publishedAt)\n      && nowMs - (alert.updatedAt ?? alert.publishedAt) <= SASKALERT_MAX_CONTENT_AGE_MIN * 60_000)\n    : [];\n  const verifiedIds = new Set(data.alerts.map(alert => alert.id));\n  const snapshot = { alerts: [...data.alerts, ...retained.filter(alert => !verifiedIds.has(alert.id))] };\n  const contentAge = { ...saskAlertContentMeta(snapshot, nowMs), maxContentAgeMin: SASKALERT_MAX_CONTENT_AGE_MIN };\n  const metaPatch = { ...diagnostics, lastAttemptAt: nowMs, retainedRecords: retained.length };\n  // An incomplete attempt can remove ended records, but cannot advance the success clock.\n  await writeExtraKey(canonicalKey, snapshot, ttlSeconds, {\n    ...previousMeta, fetchedAt, recordCount: snapshot.alerts.length,\n    sourceVersion: 'saskalert-v1', schemaVersion: 1, state: 'ERROR',\n    errorReason: 'CAP_VERIFICATION_FAILED', ...contentAge,\n  });\n  const written = await writeFreshnessMetadataSafely('alerts', 'saskalert', snapshot.alerts.length,\n    'saskalert-v1', ttlSeconds, fetchedAt, contentAge, metaPatch);\n  if (!written) throw new Error('saskalert: failed to persist verification diagnostics');\n  await rebuildCanadaAlertsUnion({ currentSource: { province: 'SK', snapshot, metaPatch: { ...metaPatch, fetchedAt } } });\n  throw Object.assign(new Error('saskalert: incomplete CAP verification; success clock unchanged'), { code: 'CAP_VERIFICATION_FAILED' });\n}\n\nexport function saskAlertPublishTransform(data) {\n  return { alerts: Array.isArray(data?.alerts) ? data.alerts : [] };\n}\n\nexport function saskAlertAfterPublish(data) {\n  const failed = Math.min(100, Math.max(0, Number(data?._capVerification?.failed) || 0));\n  const skippedDeadline = Math.min(100, Math.max(0, Number(data?._capVerification?.skippedDeadline) || 0));\n  if (failed > 0 || skippedDeadline > 0) {\n    return {\n      freshnessMetaPatch: {\n        sourceState: 'degraded',\n        errorCode: 'CAP_VERIFICATION_FAILED',\n        capVerificationFailed: failed,\n        capSkippedDeadline: skippedDeadline,\n        capFailureReasons: data._capVerification.reasons,\n      },","sourceCodeStart":376,"sourceCodeEnd":412,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/lib/saskalert.mjs#L376-L412","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n// caller treats any throw as total failure\nclass SaskAlertError extends Error { constructor(msg, code) { super(msg); this.code = code; } }\n// after\ntry {\n  await publishSaskAlerts(data, { canonicalKey, ttlSeconds });\n} catch (err) {\n  if (err?.code === 'CAP_VERIFICATION_FAILED') {\n    console.warn('SK alerts degraded; retained previous snapshot, will retry next cycle');\n    return; // do not page: success clock intentionally unchanged\n  }\n  throw err;\n}","handlingStrategy":"try-catch","validationCode":"if (data?._capVerification && (data._capVerification.failed > 0 || data._capVerification.skippedDeadline > 0)) console.warn('SK snapshot will publish as degraded: ', data._capVerification);","typeGuard":null,"tryCatchPattern":"try {\n  await saskAlertBeforePublish(data, { canonicalKey, ttlSeconds });\n} catch (err) {\n  if (err?.code === 'CAP_VERIFICATION_FAILED') {\n    // expected degraded path: previous snapshot retained, success clock unchanged\n    logger.warn({ key: canonicalKey, verification: data._capVerification }, 'SK alerts degraded');\n    return;\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["verification","degraded","cap","partial-failure"],"backgroundTag":"unexpected-response-shape","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}