{"record":{"id":"f6a08214d441b472","repo":"affaan-m/ECC","slug":"invalid-now-timestamp-now-f6a082","errorCode":null,"errorMessage":"Invalid now timestamp: ${now}","messagePattern":"Invalid now timestamp: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/lib/skill-evolution/health.js","lineNumber":149,"sourceCode":"  if (records.length === 0) {\n    return null;\n  }\n\n  return records\n    .map(record => ({\n      timestamp: record.recorded_at,\n      timeMs: Date.parse(record.recorded_at),\n    }))\n    .filter(entry => !Number.isNaN(entry.timeMs))\n    .sort((left, right) => left.timeMs - right.timeMs)\n    .at(-1)?.timestamp || null;\n}\n\nfunction collectSkillHealth(options = {}) {\n  const now = options.now || new Date().toISOString();\n  const nowMs = Date.parse(now);\n  if (Number.isNaN(nowMs)) {\n    throw new Error(`Invalid now timestamp: ${now}`);\n  }\n\n  const warnThreshold = typeof options.warnThreshold === 'number'\n    ? options.warnThreshold\n    : Number(options.warnThreshold || 0.1);\n  if (!Number.isFinite(warnThreshold) || warnThreshold < 0) {\n    throw new Error(`Invalid warn threshold: ${options.warnThreshold}`);\n  }\n\n  const records = tracker.readSkillExecutionRecords(options);\n  const skillsById = discoverSkills(options);\n  const recordsBySkill = records.reduce((groupedRecords, record) => {\n    if (!groupedRecords.has(record.skill_id)) {\n      groupedRecords.set(record.skill_id, []);\n    }\n\n    groupedRecords.get(record.skill_id).push(record);\n    return groupedRecords;","sourceCodeStart":131,"sourceCodeEnd":167,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/scripts/lib/skill-evolution/health.js#L131-L167","documentation":"Thrown by collectSkillHealth (the health module that powers the dashboard) when options.now is provided but Date.parse(now) is NaN. Identical guard to dashboard.renderDashboard because health is its upstream and threads `now` through every aggregation. Also reachable via summarizeHealthReport/calculateSuccessRate paths that accept options.now.","triggerScenarios":"Calling health.collectSkillHealth({ now: 'not-a-date' }), or any caller (dashboard, CLI) forwarding an invalid now into the health pipeline. A non-empty invalid string is the trigger; falsy values fall back to new Date().toISOString().","commonSituations":"Same as the dashboard variant: forwarding unvalidated user/clock input; locale-formatted dates; passing a Date object instead of an ISO string.","solutions":["Omit options.now or pass a strict ISO 8601 timestamp.","Pre-validate: if (Number.isNaN(Date.parse(now))) throw/fallback before calling.","Normalize user input through new Date(input).toISOString()."],"exampleFix":"// before\nhealth.collectSkillHealth({ now: rawTimestamp });\n\n// after\nhealth.collectSkillHealth({ now: new Date(rawTimestamp).toISOString() });","handlingStrategy":"validation","validationCode":"const now = opts.now && !Number.isNaN(Date.parse(opts.now)) ? opts.now : new Date().toISOString();\nhealth.collectSkillHealth({ ...opts, now });","typeGuard":"function isIsoTimestamp(v) {\n  return typeof v === 'string' && !Number.isNaN(Date.parse(v));\n}","tryCatchPattern":"try {\n  health.collectSkillHealth({ now });\n} catch (err) {\n  if (/Invalid now timestamp/.test(err.message)) health.collectSkillHealth({ ...opts });\n  else throw err;\n}","preventionTips":["Centralize timestamp normalization in one helper reused by dashboard and health callers.","Never forward raw user input as `now`.","In tests, generate now from new Date().toISOString()."],"tags":["health","timestamp","validation"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}