{"record":{"id":"8f90266a8e72cb14","repo":"koala73/worldmonitor","slug":"compact-health-pending-must-be-an-object","errorCode":null,"errorMessage":"Compact health pending must be an object","messagePattern":"Compact health pending must be an object","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/check-seed-freshness.mjs","lineNumber":20,"sourceCode":"\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { parseArgs } from 'node:util';\n\nconst DEFAULT_HEALTH_URL = 'https://api.worldmonitor.app/api/health?compact=1';\nconst BASELINE_URL = new URL('./seed-freshness-baseline.json', import.meta.url);\n// api/health.js only serves a cached verdict for 60 seconds. Allow its maximum\n// 20-second request timeout too, so a valid snapshot cannot be rejected solely\n// because the response arrived at the end of the monitor's fetch window.\nexport const MAX_HEALTH_OBSERVATION_AGE_MS = 80 * 1000;\n\nexport function validateCompactHealthPayload(payload) {\n  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {\n    throw new Error('Compact health payload must be an object');\n  }\n  if (Object.hasOwn(payload, 'pending')) {\n    if (!payload.pending || typeof payload.pending !== 'object' || Array.isArray(payload.pending)) {\n      throw new Error('Compact health pending must be an object');\n    }\n    for (const entry of Object.values(payload.pending)) {\n      if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {\n        throw new Error('Compact health pending entries must be objects');\n      }\n    }\n  }\n  // Compact health omits `problems` entirely when every check is healthy.\n  if (payload.problems == null && payload.status === 'HEALTHY') return payload;\n  if (!payload.problems || typeof payload.problems !== 'object' || Array.isArray(payload.problems)) {\n    throw new Error('Compact health payload must contain a problems object');\n  }\n  return payload;\n}\n\n// The ONLY states being on-demand actually explains: nothing has requested the\n// key yet, or the producer has not run for the first time. Absence is expected\n// for an RPC-populated cache or a deployment-order bridge, so it must not page.","sourceCodeStart":2,"sourceCodeEnd":38,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/check-seed-freshness.mjs#L2-L38","documentation":"Within validateCompactHealthPayload, if the compact health object carries a `pending` key it must be a non-null, non-array object whose values are entry objects. This check throws when `pending` is present but is null, an array, or a primitive, preventing downstream Object.values iteration from failing or silently mis-measuring pending seeds.","triggerScenarios":"Health endpoint emitting `pending: null` (serialization of undefined), `pending: []` (array instead of keyed map), or `pending: \"none\"`; caller constructing the payload manually with a wrong-shaped pending field.","commonSituations":"API change where pending became a list of objects; a stub/mock fixture using an array; JSON produced by code that sets pending to null when empty instead of omitting the key.","solutions":["Fix the health endpoint/serializer to omit `pending` when empty or emit it as a keyed object","If pending is now a list by design, update validateCompactHealthPayload and its consumers to the new shape","Correct test fixtures/mocks to use an object map for pending","Guard at the producer side: only include `pending` when it is a populated object"],"exampleFix":"// before\n{\"status\":\"HEALTHY\",\"pending\":null}\n// after\n{\"status\":\"HEALTHY\"}","handlingStrategy":"type-guard","validationCode":"if ('pending' in payload && (payload.pending === null || typeof payload.pending !== 'object' || Array.isArray(payload.pending))) throw new Error('pending must be a keyed object when present');","typeGuard":"const hasValidPending = (p) => !('pending' in p) || (p.pending !== null && typeof p.pending === 'object' && !Array.isArray(p.pending));","tryCatchPattern":"try {\n  validateCompactHealthPayload(payload);\n} catch (e) {\n  if (e.message === 'Compact health pending must be an object') {\n    console.error('pending shape drifted from the compact health contract');\n  } else throw e;\n}","preventionTips":["Omit the pending key entirely when there are no pending seeds","Never serialize pending as null or an array","Keep worker and checker deployed from the same contract/schema","Add fixtures tests asserting pending's object shape"],"tags":["validation","schema","health","json"],"backgroundTag":"schema-validation-failed","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"}