{"record":{"id":"5e334b33648ff56b","repo":"jackwener/OpenCLI","slug":"date-must-be-a-real-calendar-date-date","errorCode":null,"errorMessage":"Date must be a real calendar date: ${date}","messagePattern":"Date must be a real calendar date: (.+?)","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"clis/mercury/utils.js","lineNumber":76,"sourceCode":"    const currency = optionalString(kwargs, 'currency', 'CNY').toUpperCase();\n    if (!/^[A-Z]{3}$/.test(currency)) {\n        throw new ArgumentError(`Currency must be a three-letter ISO currency code: ${currency}`);\n    }\n    return currency;\n}\n\nfunction parseDate(kwargs) {\n    const date = requireString(kwargs, 'date');\n    const match = date.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n    if (!match) {\n        throw new ArgumentError(`Date must be YYYY-MM-DD: ${date}`);\n    }\n    const year = Number(match[1]);\n    const month = Number(match[2]);\n    const day = Number(match[3]);\n    const parsed = new Date(Date.UTC(year, month - 1, day));\n    if (parsed.getUTCFullYear() !== year || parsed.getUTCMonth() !== month - 1 || parsed.getUTCDate() !== day) {\n        throw new ArgumentError(`Date must be a real calendar date: ${date}`);\n    }\n    return date;\n}\n\nfunction parseOcrWaitSeconds(kwargs) {\n    const raw = optionalString(kwargs, 'ocr-wait-seconds', '8');\n    if (!/^\\d+$/.test(raw)) {\n        throw new ArgumentError(`ocr-wait-seconds must be a non-negative integer: ${raw}`);\n    }\n    return Number(raw);\n}\n\nexport function normalizeReimbursementInput(kwargs) {\n    return {\n        receipt: parseReceiptPath(kwargs),\n        amount: parseAmount(kwargs),\n        currency: parseCurrency(kwargs),\n        date: parseDate(kwargs),","sourceCodeStart":58,"sourceCodeEnd":94,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/mercury/utils.js#L58-L94","documentation":"After the YYYY-MM-DD shape check passes, parseDate constructs a UTC Date and verifies the components round-trip (year/month/day unchanged). This rejects impossible calendar dates like 2026-02-31 or 2025-13-01, which the regex alone would accept. JavaScript's Date silently rolls such values over, so the round-trip check is the real validity gate.","triggerScenarios":"--date '2026-02-30', '2026-02-31', '2025-13-01', '2026-00-10', '2026-04-31' — syntactically valid shape but not a real date.","commonSituations":"Hand-typed dates with wrong day counts (February 30/31); month off-by-one from zero-indexed month values in generated scripts; day 31 in 30-day months; scripts computing dates incorrectly before passing them in.","solutions":["Correct the date to a real calendar date (check the month's day count).","Generate dates programmatically instead of hardcoding: new Date(Date.UTC(y,m-1,d)).toISOString().slice(0,10).","If constructing from zero-indexed months in JS, add 1 to the month before formatting.","Validate upstream with a calendar-aware check (e.g. Date round-trip) before calling."],"exampleFix":"// before\n--date \"2026-02-30\"\n// after\n--date \"2026-02-28\"","handlingStrategy":"validation","validationCode":"function isRealDate(d) {\n  const m = /^\\d{4}-\\d{2}-\\d{2}$/.exec(d);\n  if (!m) return false;\n  const dt = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));\n  return dt.getUTCFullYear() === +m[1] && dt.getUTCMonth() === +m[2] - 1 && dt.getUTCDate() === +m[3];\n}","typeGuard":null,"tryCatchPattern":"try {\n  await reimburse({ date });\n} catch (e) {\n  if (e instanceof ArgumentError && e.message.includes('real calendar date')) {\n    console.error(`'${date}' is not a valid calendar date (e.g. Feb 30).`);\n  } else throw e;\n}","preventionTips":["Derive dates from Date arithmetic, not hand-typed literals","Remember JS months are 0-indexed when constructing dates","Validate month/day ranges before submitting forms","Use a date library (or this round-trip check) in upstream tooling"],"tags":["argument-validation","date-validation","input-validation","calendar"],"backgroundTag":"invalid-date","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}