{"record":{"id":"38510734111050d7","repo":"jackwener/OpenCLI","slug":"budgetms-must-be-a-positive-number-got-json-str","errorCode":null,"errorMessage":"budgetMs must be a positive number, got ${JSON.stringify(budgetMs)}","messagePattern":"budgetMs must be a positive number, got (.+?)","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"clis/xiaohongshu/search.js","lineNumber":634,"sourceCode":"        const stripXhsAuthorDateSuffix = ${stripXhsAuthorDateSuffix.toString()};\n        const extractSearchRows = ${extractSearchRows.toString()};\n        return extractSearchRows(${JSON.stringify(webHost)});\n      })()\n    `;\n}\n\nexport function buildScrollHarvestJs(webHost, targetCount, options = {}) {\n    const maxRounds = options.maxRounds ?? 30;\n    const budgetMs = options.budgetMs ?? 30_000;\n    const step = options.step ?? DEFAULT_HARVEST_STEP;\n    if (!Number.isSafeInteger(targetCount) || targetCount < 1) {\n        throw new ArgumentError(`targetCount must be a positive integer, got ${JSON.stringify(targetCount)}`);\n    }\n    if (!Number.isSafeInteger(maxRounds) || maxRounds < 1) {\n        throw new ArgumentError(`maxRounds must be a positive integer, got ${JSON.stringify(maxRounds)}`);\n    }\n    if (!Number.isFinite(budgetMs) || budgetMs <= 0) {\n        throw new ArgumentError(`budgetMs must be a positive number, got ${JSON.stringify(budgetMs)}`);\n    }\n    if (!Number.isFinite(step) || step < 0) {\n        throw new ArgumentError(`step must be a non-negative number, got ${JSON.stringify(step)}`);\n    }\n    return `\n      (async () => {\n        const targetCount = ${targetCount};\n        const maxRounds = ${maxRounds};\n        const budgetMs = ${budgetMs};\n        const configuredStep = ${step};\n        const webHost = ${JSON.stringify(webHost)};\n        const noteUrlInfo = ${noteUrlInfo.toString()};\n        const mergeHarvestedRow = ${mergeHarvestedRow.toString()};\n        const stripXhsAuthorDateSuffix = ${stripXhsAuthorDateSuffix.toString()};\n        const extractSearchRows = ${extractSearchRows.toString()};\n        const usableRowCount = ${usableRowCount.toString()};\n        const shouldStopScrolling = ${shouldStopScrolling.toString()};\n        const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));","sourceCodeStart":616,"sourceCodeEnd":652,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/xiaohongshu/search.js#L616-L652","documentation":"buildScrollHarvestJs validates budgetMs as a finite number > 0 (default 30_000 via options.budgetMs) and throws this ArgumentError otherwise. budgetMs is the wall-clock time budget injected into the harvest script, so a zero, negative, or non-finite value would make the time-loop logic nonsensical and is rejected at build time.","triggerScenarios":"Passing options { budgetMs: 0 }, a negative duration, NaN/Infinity, or a non-number (e.g. '30000' string) to buildScrollHarvestJs.","commonSituations":"Parsing a duration string like '30s' and passing the raw string; config value 0 intended as 'no budget'; multiplying undefined by 1000 yielding NaN; passing seconds (30) while assuming the API converts units — use milliseconds.","solutions":["Pass a finite positive number of milliseconds, e.g. { budgetMs: 30_000 }.","Convert duration strings to ms before passing (30s -> 30000).","Omit options.budgetMs to use the 30s default.","Sanitize with Number() and Number.isFinite before constructing options."],"exampleFix":"// before\nbuildScrollHarvestJs(host, target, { budgetMs: config.timeoutSeconds });\n// after\nconst budgetMs = Number(config.timeoutSeconds) * 1000;\nif (!Number.isFinite(budgetMs) || budgetMs <= 0) throw new Error('bad budget');\nbuildScrollHarvestJs(host, target, { budgetMs });","handlingStrategy":"validation","validationCode":"function assertBudgetMs(v) {\n  if (!Number.isFinite(v) || v <= 0) {\n    throw new TypeError(`budgetMs must be a positive finite number, got ${JSON.stringify(v)}`);\n  }\n}\nassertBudgetMs(options.budgetMs ?? 30_000);","typeGuard":"const isValidBudgetMs = (v) => v === undefined || (Number.isFinite(v) && v > 0);","tryCatchPattern":null,"preventionTips":["Always pass milliseconds, not seconds or duration strings.","Convert strings like '30s' to numbers before passing.","Check Number.isFinite — Infinity and NaN both fail validation.","Default to 30_000 rather than hand-rolling small values like 0.","Centralize duration parsing in one utility to avoid unit mistakes."],"tags":["argument-validation","typeerror","api-misuse"],"backgroundTag":"invalid-argument","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}