{"record":{"id":"60815f5fc096c253","repo":"jackwener/OpenCLI","slug":"semanticscholar-citations-offset-must-be-9999","errorCode":null,"errorMessage":"semanticscholar citations offset must be <= 9999","messagePattern":"semanticscholar citations offset must be <= 9999","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"clis/semanticscholar/citations.js","lineNumber":41,"sourceCode":"    domain: 'api.semanticscholar.org',\n    strategy: Strategy.PUBLIC,\n    browser: false,\n    args: [\n        { name: 'id', positional: true, required: true, help: 'paperId (40-char hex), DOI, arXiv id, or prefixed id' },\n        { name: 'limit', type: 'int', default: 20, help: 'Max citing papers (1-1000, single Semantic Scholar page)' },\n        { name: 'offset', type: 'int', default: 0, help: 'Page offset (0-based)' },\n    ],\n    columns: ['rank', 'paperId', 'doi', 'title', 'year', 'firstAuthor', 'citationCount', 'url'],\n    func: async (args) => {\n        const ref = requirePaperRef(args.id);\n        const limit = requireBoundedInt(args.limit, 20, 1000);\n        const offsetRaw = args.offset ?? 0;\n        const offset = typeof offsetRaw === 'number' ? offsetRaw : Number(offsetRaw);\n        if (!Number.isInteger(offset) || offset < 0) {\n            throw new ArgumentError('semanticscholar citations offset must be a non-negative integer');\n        }\n        if (offset > 9999) {\n            throw new ArgumentError('semanticscholar citations offset must be <= 9999');\n        }\n        const url = `${S2_GRAPH_BASE}/paper/${encodeURIComponent(ref)}/citations?fields=${FIELDS}&limit=${limit}&offset=${offset}`;\n        const body = await s2Fetch(url, 'semanticscholar citations');\n\n        const data = Array.isArray(body?.data) ? body.data : null;\n        if (data === null) {\n            throw new CommandExecutionError('semanticscholar citations returned an unexpected payload shape');\n        }\n        if (!data.length) {\n            throw new EmptyResultError('semanticscholar citations', `No Semantic Scholar citations for \"${args.id}\" at offset ${offset}.`);\n        }\n\n        return data.slice(0, limit).map((entry, i) => {\n            if (!entry || typeof entry !== 'object' || !('citingPaper' in entry)) {\n                throw new CommandExecutionError('semanticscholar citations row is missing citingPaper');\n            }\n            return normalizePaperRow(entry.citingPaper, 'citations', { rank: offset + i + 1 });\n        });","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/semanticscholar/citations.js#L23-L59","documentation":"The semanticscholar citations command caps the pagination offset at 9999 (clis/semanticscholar/citations.js:41) because the Semantic Scholar Graph API itself will not serve pages beyond that depth. Values above 9999 are rejected locally with an ArgumentError before any request is made.","triggerScenarios":"Calling `semanticscholar citations <id> --offset 10000` or higher, typically from a naive auto-pagination loop that keeps incrementing offset until an EmptyResultError instead of stopping at the cap.","commonSituations":"Bulk-harvesting citation lists of heavily-cited papers (tens of thousands of citations) with a `while` loop; computing offset as result-count without clamping; migrating code from another API without page limits.","solutions":["Clamp offset to <= 9999 in your pagination loop and stop paging there.","Narrow the result set instead of deep-paging (e.g. filter by year using other endpoints) to reach items below the cap.","Break the loop when offset + limit would exceed 9999.","For very large citation lists, fetch via the Semantic Scholar API/datasets directly rather than unbounded paging."],"exampleFix":"// before\nwhile (true) { await fetchPage(offset); offset += limit; }\n// after\nwhile (offset <= 9999) {\n  await fetchPage(offset);\n  offset += limit;\n  if (offset > 9999) break; // API caps pagination at 9999\n}","handlingStrategy":"validation","validationCode":"const S2_OFFSET_CAP = 9999;\nfunction clampOffset(raw) {\n  const n = typeof raw === 'number' ? raw : Number(raw);\n  return Math.min(Math.max(0, Number.isInteger(n) ? n : 0), S2_OFFSET_CAP);\n}\n// use before calling: clampOffset(args.offset)","typeGuard":"function isWithinOffsetCap(v) {\n  const n = typeof v === 'number' ? v : Number(v);\n  return Number.isInteger(n) && n >= 0 && n <= 9999;\n}","tryCatchPattern":"try {\n  await cli('semanticscholar citations', id, { offset });\n} catch (err) {\n  if (/offset must be <= 9999/.test(err.message)) {\n    console.error('S2 caps pagination at offset 9999; stopping deep paging.');\n    return; // end collection here\n  }\n  throw err;\n}","preventionTips":["Stop pagination loops when offset + limit > 9999 instead of waiting for an error.","Clamp offsets programmatically before every call.","For very large citation lists, use the S4 datasets/API export rather than deep paging.","Never compute offset from unbounded counters."],"tags":["validation","pagination","limits","semanticscholar"],"backgroundTag":"offset-exceeds-api-limit","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}