{"record":{"id":"0ea0802319ecf383","repo":"koala73/worldmonitor","slug":"physical-divergence-snapshot-must-contain-gold-and","errorCode":null,"errorMessage":"Physical divergence snapshot must contain gold and silver readings","messagePattern":"Physical divergence snapshot must contain gold and silver readings","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"server/_shared/physical-divergence-snapshot.ts","lineNumber":449,"sourceCode":"export 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":431,"sourceCodeEnd":460,"githubUrl":"https://github.com/koala73/worldmonitor/blob/9361220cc013571781071f0206e4d80fd14b2f7f/server/_shared/physical-divergence-snapshot.ts#L431-L460","documentation":"normalizePhysicalDivergenceSnapshot validates that a stored physical-divergence snapshot envelope contains exactly one reading per metal in PHYSICAL_DIVERGENCE_METALS (gold and silver, per the contract's metalOrder). It throws a TypeError when the readings array has the wrong length or contains duplicate/missing metals, because the stress composite cannot be computed from an incomplete or duplicated metal set. This is a data-integrity guard against corrupted or hand-edited snapshots.","triggerScenarios":"raw.readings maps to fewer entries than PHYSICAL_DIVERGENCE_METALS.length, or two readings carry the same metal value (e.g. both 'gold'), or a reading's metal field was dropped/renamed during serialization so the distinct-metal count mismatches.","commonSituations":"A Redis snapshot written by an older schema version missing one metal; a manual seed edit that duplicated gold and omitted silver; a migration or copy/paste that truncated the readings array; an upstream writer bug that serializes metals with different casing or keys.","solutions":["Re-seed or rewrite the snapshot so it contains exactly one reading for each metal in PHYSICAL_DIVERGENCE_METALS (gold and silver)","Check the writer that produced the snapshot: it must emit one reading per contract metal, using the canonical metal identifiers","Check for schema drift: if PHYSICAL_DIVERGENCE_METALS changed (contract metalOrder), regenerate all stored snapshots to match","Validate the raw payload with the same length/distinct-metal check before persisting it"],"exampleFix":"// before\nreadings: [{ metal: 'gold', price: 2300 }]\n// after\nreadings: [\n  { metal: 'gold', ... },\n  { metal: 'silver', ... }\n]","handlingStrategy":"validation","validationCode":"function isValidSnapshotReadings(readings) {\n  return Array.isArray(readings)\n    && readings.length === PHYSICAL_DIVERGENCE_METALS.length\n    && new Set(readings.map((r) => r.metal)).size === PHYSICAL_DIVERGENCE_METALS.length;\n}\n// run before normalizePhysicalDivergenceSnapshot on any stored payload","typeGuard":"function hasAllMetals(readings): readings is PhysicalDivergenceReading[] {\n  return Array.isArray(readings)\n    && PHYSICAL_DIVERGENCE_METALS.every((m) => readings.some((r) => r?.metal === m));\n}","tryCatchPattern":"try {\n  const snap = normalizePhysicalDivergenceSnapshot(raw, nowMs);\n} catch (e) {\n  if (e instanceof TypeError && /gold and silver/.test(e.message)) {\n    snap = await reseedSnapshot(); // regenerate from source\n  } else throw e;\n}","preventionTips":["Always write snapshots with one reading per metal in PHYSICAL_DIVERGENCE_METALS","Validate payloads with the length/distinct-metal check before persisting to Redis","Regenerate stored snapshots whenever the metals contract changes","Add a seed-write test asserting both gold and silver readings exist"],"tags":["data-integrity","validation","schema","redis"],"backgroundTag":"snapshot-schema-mismatch","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"}