{"record":{"id":"9d7e0c264b14fc90","repo":"koala73/worldmonitor","slug":"physical-divergence-snapshot-has-an-invalid-envelo","errorCode":null,"errorMessage":"Physical divergence snapshot has an invalid envelope","messagePattern":"Physical divergence snapshot has an invalid envelope","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"server/_shared/physical-divergence-snapshot.ts","lineNumber":443,"sourceCode":"/**\n * A snapshot written under a methodology this build does not implement. Distinct from an\n * unknown state: this is ordinary producer/consumer deploy skew (the Railway seeder and the\n * Vercel API ship independently), so a read path should fail closed with a reason rather\n * than 500 for the length of the rollout window.\n */\nexport function isUnsupportedPhysicalDivergenceMethodology(error: unknown): boolean {\n  return error instanceof Error\n    && error.message.startsWith('Unsupported physical divergence methodology:');\n}\n\nexport function normalizePhysicalDivergenceSnapshot(\n  value: unknown,\n  nowMs = Date.now(),\n): PhysicalDivergenceRawSnapshot {\n  const raw = object(value);\n  methodology(raw.methodologyVersion);\n  if (!isoInstant(raw.evaluatedAt) || !Array.isArray(raw.readings) || !Array.isArray(raw.transitions)) {\n    throw new TypeError('Physical divergence snapshot has an invalid envelope');\n  }\n  const storedReadings = raw.readings.map(reading);\n  if (\n    storedReadings.length !== PHYSICAL_DIVERGENCE_METALS.length\n    || new Set(storedReadings.map((entry) => entry.metal)).size !== PHYSICAL_DIVERGENCE_METALS.length\n  ) throw new TypeError('Physical divergence snapshot must contain gold and silver readings');\n  validateStoredComposite(raw.composite, storedReadings);\n  const readings = applyFreshness(storedReadings, nowMs);\n  return {\n    readings,\n    composite: buildPhysicalStressComposite(readings),\n    evaluatedAt: raw.evaluatedAt,\n    methodologyVersion: PHYSICAL_DIVERGENCE_METHODOLOGY_VERSION,\n    transitions: raw.transitions.map(transition),\n  };\n}\n","sourceCodeStart":425,"sourceCodeEnd":460,"githubUrl":"https://github.com/koala73/worldmonitor/blob/9361220cc013571781071f0206e4d80fd14b2f7f/server/_shared/physical-divergence-snapshot.ts#L425-L460","documentation":"normalizePhysicalDivergenceSnapshot() first coerces the input to an object, validates the methodology version, then requires evaluatedAt to be a valid ISO instant and readings/transitions to be arrays. If any of these envelope-level checks fail it throws this TypeError, rejecting the snapshot before per-reading validation even begins. It is the top-level shape gate for the stored dataset.","triggerScenarios":"getPhysicalDivergenceIndex / normalizePhysicalDivergenceDataset receiving cached or fetched data where evaluatedAt is missing/malformed (not an ISO 8601 instant), readings or transitions is not an array, or value isn't even an object — e.g. a JSON string was stored instead of a parsed object, or an error payload was cached.","commonSituations":"A cache entry holding `{ body: \"...\" }` wrapper JSON; double-stringified Redis values; an upstream error page cached in the snapshot key; schema drift after a producer change; empty/null cache reads passed straight through.","solutions":["Inspect the actual stored value at the snapshot cache key; purge the corrupt entry and let it regenerate from live data.","Ensure the writer stores the parsed object (not JSON.stringify'd twice) with evaluatedAt as an ISO instant and readings/transitions arrays.","If an error/empty response can be cached, guard the writer to only cache successfully normalized snapshots.","Version the cache key with the methodology/snapshot schema version so old shapes are never re-read by new code."],"exampleFix":"// before\ncache.set(key, JSON.stringify(snapshot)); // later read as string\nnormalizePhysicalDivergenceSnapshot(raw); // throws: not an object\n// after\nconst value = JSON.parse(raw); // parse once at the boundary\nnormalizePhysicalDivergenceSnapshot(value);","handlingStrategy":"type-guard","validationCode":"function looksLikeDivergenceEnvelope(v: unknown): boolean {\n  return !!v && typeof v === 'object'\n    && typeof (v as any).evaluatedAt === 'string' && !Number.isNaN(Date.parse((v as any).evaluatedAt))\n    && Array.isArray((v as any).readings) && Array.isArray((v as any).transitions);\n}\nif (!looksLikeDivergenceEnvelope(cached)) await refreshFromSource();","typeGuard":"function isDivergenceEnvelope(v: unknown): v is { evaluatedAt: string; readings: unknown[]; transitions: unknown[]; methodologyVersion: string } {\n  return !!v && typeof v === 'object'\n    && typeof (v as any).evaluatedAt === 'string' && !Number.isNaN(Date.parse((v as any).evaluatedAt))\n    && Array.isArray((v as any).readings) && Array.isArray((v as any).transitions);\n}","tryCatchPattern":"try {\n  const snapshot = normalizePhysicalDivergenceSnapshot(raw, Date.now());\n} catch (err) {\n  if (err instanceof TypeError && err.message.includes('invalid envelope')) {\n    await cache.delete(key);            // purge corrupt entry\n    snapshot = await fetchAndStoreFresh();\n  } else throw err;\n}","preventionTips":["Parse JSON once at the cache boundary; never store double-stringified values.","Only cache successfully normalized snapshots — never raw upstream/error payloads.","Version cache keys with the snapshot schema/methodology version.","Validate evaluatedAt with a strict ISO-instant check before writing."],"tags":["data-integrity","validation","cache","snapshot","physical-divergence"],"backgroundTag":"snapshot-validation-failed","analyzedSha":"9361220cc013571781071f0206e4d80fd14b2f7f","analyzedAt":"2026-09-01T10:32:37.851Z","contentChangedAt":"2026-09-01T10:32:37.851Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}