{"record":{"id":"3f652c4cd1f7a2aa","repo":"koala73/worldmonitor","slug":"compact-health-pending-entries-must-be-objects","errorCode":null,"errorMessage":"Compact health pending entries must be objects","messagePattern":"Compact health pending entries must be objects","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/check-seed-freshness.mjs","lineNumber":24,"sourceCode":"\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.\n//\n// Everything else must stay strict even for an on-demand source. `SEED_ERROR`\n// means the producer ran and failed; a long `STALE_SEED` means it stopped\n// running. Neither is explained by \"nobody asked for it yet\", and softening","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/check-seed-freshness.mjs#L6-L42","documentation":"validateCompactHealthPayload also requires every value inside `pending` to be a non-null, non-array object — one entry per pending seed with its own fields. It throws when any entry is null, an array, or a scalar, because downstream freshness logic reads per-seed properties from each entry and cannot do so safely otherwise.","triggerScenarios":"A pending map whose values are scalars (e.g. {\"seed:foo\": 123} timestamps instead of objects) or nulls; manual fixture construction with wrong entry shape; API change flattening entries into plain values.","commonSituations":"Producer simplifies entries to timestamps/strings to save payload size; test mocks built with placeholder primitives; JSON schema drift between worker and checker versions deployed at different times.","solutions":["Fix the producer so each pending value is an object with the expected per-seed fields","Update fixtures/mocks to use full entry objects","If the wire format intentionally changed, update validateCompactHealthPayload and downstream readers together in one deploy","Add a schema test on the worker output so entry shape drift is caught before the checker runs"],"exampleFix":"// before\n{\"pending\":{\"seed:fx\": 1719000000000}}\n// after\n{\"pending\":{\"seed:fx\":{\"updatedAt\":1719000000000,\"status\":\"PENDING\"}}}","handlingStrategy":"type-guard","validationCode":"const entries = Object.values(payload.pending ?? {});\nif (entries.some((e) => e === null || typeof e !== 'object' || Array.isArray(e))) throw new Error('each pending entry must be an object');","typeGuard":"const pendingEntriesAreObjects = (p) => Object.values(p.pending ?? {}).every((e) => e !== null && typeof e === 'object' && !Array.isArray(e));","tryCatchPattern":"try {\n  validateCompactHealthPayload(payload);\n} catch (e) {\n  if (e.message === 'Compact health pending entries must be objects') {\n    console.error('A pending seed entry lost its object shape — check the producer');\n  } else throw e;\n}","preventionTips":["Give every pending seed a full entry object with its fields","Validate producer output against the schema in worker tests","Deploy worker and checker contract changes atomically","Keep mock fixtures shape-identical to production output"],"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"}