{"record":{"id":"33669c20938754d1","repo":"jackwener/OpenCLI","slug":"chess-com-game-archive-entry-is-missing-a-stable-g","errorCode":null,"errorMessage":"Chess.com game archive entry is missing a stable game URL","messagePattern":"Chess\\.com game archive entry is missing a stable game URL","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/chess/utils.js","lineNumber":124,"sourceCode":"export function openingName(eco) {\n    if (typeof eco !== 'string' || !eco.startsWith('http')) return '';\n    const tail = eco.replace(/\\/+$/, '').split('/').pop() || '';\n    if (!tail) return '';\n    const namePart = tail.match(/^([^.]+?)(?:-\\d|\\.\\.\\.|$)/);\n    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 || '';","sourceCodeStart":106,"sourceCodeEnd":142,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/chess/utils.js#L106-L142","documentation":"mapGameRow() requires every game entry to carry a stable canonical URL matching https://www.chess.com/game/live/<digits> or /game/daily/<digits>; this URL is used as the row's unique key. This throw fires when game.url is missing, not a string, or points at a different game host/format (e.g. /game/<id> without live|daily, chess.com/live/<id>, or a relative link).","triggerScenarios":"Archive entry from row()'s iteration has no url field, an empty string, or a URL that fails the /game/(live|daily)/<digits> regex — such as /game/live/ (no id), /live/<id>, or https://api.chess.com links.","commonSituations":"Chess.com changes its public game URL scheme; entries sourced from the older /chess.com/game/... format; hand-built fixtures omitting url; data scraped from a different surface (analysis links, puzzles) that has no canonical game URL.","solutions":["Log the offending game.url value and compare against the expected https://www.chess.com/game/live/<id> or /game/daily/<id> format","Re-fetch the monthly archive from the pub API — archived games normally always include the canonical URL","Normalize alternative URL shapes to the canonical form before calling mapGameRow","Skip entries without a stable URL if you only need rows for well-formed games"],"exampleFix":"// before\nmapGameRow({ ...game, url: `https://www.chess.com/live/${game.id}` }, user);\n// after\nmapGameRow({ ...game, url: `https://www.chess.com/game/live/${game.id}` }, user);","handlingStrategy":"validation","validationCode":"const GAME_URL_RE = /^https:\\/\\/www\\.chess\\.com\\/game\\/(?:live|daily)\\/\\d+(?:$|[/?#])/i;\nif (typeof game?.url !== 'string' || !GAME_URL_RE.test(game.url)) {\n  throw new Error(`game has no stable URL: ${JSON.stringify(game?.url)}`);\n}","typeGuard":"function hasStableGameUrl(g) {\n  return typeof g?.url === 'string' &&\n    /^https:\\/\\/www\\.chess\\.com\\/game\\/(?:live|daily)\\/\\d+(?:$|[/?#])/i.test(g.url);\n}\n// usage: games.filter(hasStableGameUrl).map((g) => mapGameRow(g, user));","tryCatchPattern":"try {\n  row = mapGameRow(game, user);\n} catch (err) {\n  if (String(err.message).includes('missing a stable game URL')) {\n    console.warn(`Skipping game with bad url: ${game?.url}`);\n  } else { throw err; }\n}","preventionTips":["Normalize any known URL variants to the /game/live/<id> or /game/daily/<id> form before mapping","Never synthesize URLs in a different format than the canonical pub API one","Filter entries without a stable URL instead of assuming all archive entries qualify","Watch for Chess.com URL-scheme changes and update the regex if the format drifts"],"tags":["api","url-validation","chess-com","games"],"backgroundTag":"missing-required-field","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}