{"record":{"id":"f73fd4f4b3e1a714","repo":"jackwener/OpenCLI","slug":"chess-com-game-archive-entry-has-malformed-player","errorCode":null,"errorMessage":"Chess.com game archive entry has malformed player objects","messagePattern":"Chess\\.com game archive entry has malformed player objects","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/chess/utils.js","lineNumber":129,"sourceCode":"    const cleaned = (namePart ? namePart[1] : tail).replace(/-/g, ' ').trim();\n    return cleaned;\n}\n\n/**\n * Map a Chess.com game record (from the monthly archive) to a flat row.\n * The viewer perspective controls win/loss orientation.\n */\nexport function mapGameRow(game, viewerUsername) {\n    if (!isPlainObject(game)) {\n        throw new CommandExecutionError('Chess.com game archive entry is not an object');\n    }\n    if (typeof game.url !== 'string' || !/^https:\\/\\/www\\.chess\\.com\\/game\\/(?:live|daily)\\/\\d+(?:$|[/?#])/i.test(game.url)) {\n        throw new CommandExecutionError('Chess.com game archive entry is missing a stable game URL');\n    }\n    const white = game?.white || {};\n    const black = game?.black || {};\n    if (!isPlainObject(white) || !isPlainObject(black)) {\n        throw new CommandExecutionError('Chess.com game archive entry has malformed player objects');\n    }\n    if (typeof white.username !== 'string' || !white.username.trim() || typeof black.username !== 'string' || !black.username.trim()) {\n        throw new CommandExecutionError('Chess.com game archive entry is missing stable player identities');\n    }\n    const viewerLower = String(viewerUsername || '').toLowerCase();\n    const viewerIsWhite = String(white.username || '').toLowerCase() === viewerLower;\n    const viewerIsBlack = String(black.username || '').toLowerCase() === viewerLower;\n    if (!viewerIsWhite && !viewerIsBlack) {\n        throw new CommandExecutionError('Chess.com game archive entry does not include the requested player');\n    }\n    const me = viewerIsWhite ? white : black;\n    const opp = viewerIsWhite ? black : white;\n    const eco = game?.eco || '';\n    return {\n        date: formatDate(game?.end_time),\n        time_class: game?.time_class || '',\n        rated: game?.rated === true,\n        my_color: viewerIsWhite ? 'white' : 'black',","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/chess/utils.js#L111-L147","documentation":"After validating the game URL, mapGameRow() requires both game.white and game.black to be plain objects so it can read usernames, ratings, and results for each side. This throw fires when either player field is missing (and thus coerced via || {}) in a way that fails the object check — i.e. it is a non-object truthy value such as a string, number, or array.","triggerScenarios":"Archive entry where game.white or game.black is a truthy non-object (e.g. white: \"hikaru\" or white: [1500]) instead of { username, rating, result }, encountered while mapping rows for a player.","commonSituations":"A pre-processing step collapsed player objects into usernames or ratings; hand-written fixtures using shorthand; Chess.com schema drift on archive entries; passing entries from a different endpoint (e.g. a game summary endpoint with different player representation).","solutions":["Re-fetch the monthly archive — pub archive entries always carry white/black as objects","Fix the intermediate transform that replaced player objects with scalars","In fixtures use white: { username: 'a', rating: 1500, result: 'win' } shape","Add a filter that drops entries failing the player-object shape before mapping"],"exampleFix":"// before\nconst game = { url, white: 'hikaru', black: { username: 'magnus' } };\n// after\nconst game = { url, white: { username: 'hikaru', rating: 2800, result: 'win' }, black: { username: 'magnus', rating: 2850, result: 'loss' } };","handlingStrategy":"validation","validationCode":"if (!isPlain(game?.white) || !isPlain(game?.black)) {\n  throw new Error('game.white and game.black must be player objects');\n}\nfunction isPlain(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); }","typeGuard":"function hasPlayerObjects(g) {\n  const plain = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);\n  return plain(g?.white) && plain(g?.black);\n}","tryCatchPattern":"try {\n  row = mapGameRow(game, user);\n} catch (err) {\n  if (String(err.message).includes('malformed player objects')) {\n    console.warn('Skipping entry: white/black are not player objects');\n  } else { throw err; }\n}","preventionTips":["Keep white/black as full objects { username, rating, result }; never collapse to usernames","Validate archive entries as they enter your cache/pipeline","Mirror pub API shapes exactly in fixtures","Re-fetch from the pub archive when a stored entry fails shape checks"],"tags":["api","schema-validation","chess-com","games"],"backgroundTag":"schema-validation-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}