{"record":{"id":"dcc8ef05754e87c2","repo":"JuliusBrussee/caveman","slug":"stats-days-must-be-between-0-and-3660","errorCode":null,"errorMessage":"stats days must be between 0 and 3660","messagePattern":"stats days must be between 0 and 3660","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/internal/store/stats.go","lineNumber":197,"sourceCode":"\t\t\tvalue := float64(delta) / 1_000_000 * rate\n\t\t\tif finiteStatsNumber(value) && math.Abs(value) <= math.MaxFloat64/10_000_000_000 {\n\t\t\t\tvalue = cost.RoundUSD(value)\n\t\t\t\trec.RequestEstimatedInputDeltaUSD = &value\n\t\t\t} else {\n\t\t\t\trec.RequestEstimatedInputDeltaUSD = nil\n\t\t\t}\n\t\t} else {\n\t\t\trec.RequestEstimatedInputDeltaUSD = nil\n\t\t}\n\t}\n}\n\n// BuildStatsReport reads the request ledger only. Transcript imports are not\n// unioned into this report: without an exact request join that would double count\n// routed traffic and attach savings to unrelated agent usage.\nfunc (s *Store) BuildStatsReport(opts StatsReportOptions) (StatsReport, error) {\n\tif opts.Days < 0 || opts.Days > 3660 {\n\t\treturn StatsReport{}, fmt.Errorf(\"stats days must be between 0 and 3660\")\n\t}\n\tnow := opts.Now.UTC()\n\tif opts.Now.IsZero() {\n\t\tnow = time.Now().UTC()\n\t}\n\tout := StatsReport{\n\t\tSchema: \"caveman.stats.v1\", GeneratedAt: now.Format(time.RFC3339),\n\t\tWindow: StatsWindow{To: now.Format(time.RFC3339), Days: opts.Days, Lifetime: opts.Days == 0},\n\t\tGroups: []StatsGroup{}, Daily: []StatsGroup{}, Providers: []StatsGroup{}, Models: []StatsGroup{}, Agents: []StatsGroup{}, AuthModes: []StatsGroup{}, Receipts: []StatsReceipt{},\n\t\tEvidence: StatsEvidence{Basis: \"inferred\", TokenBasis: \"estimated_request_json_o200k_v1\", SpendBasis: \"provider_counted_x_snapshotted_published_rate\", CatalogVersions: []string{}, MeasurementStatuses: map[string]int64{}, ReceiptsLimit: statsReceiptLimit},\n\t}\n\t// Older ChatGPT rows used RFC3339 while the generic route used storeTSLayout.\n\t// Compare instants, not their differing separators or timezone offsets.\n\twhere := []string{\"julianday(ts) <= julianday(?)\"}\n\targs := []any{now.Format(time.RFC3339Nano)}\n\tif opts.Days > 0 {\n\t\tfrom := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, 1-opts.Days)\n\t\tout.Window.From = from.Format(time.RFC3339)","sourceCodeStart":179,"sourceCodeEnd":215,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/proxy/internal/store/stats.go#L179-L215","documentation":"BuildStatsReport validates StatsReportOptions.Days before reading the request ledger, accepting only 0 (all time) through 3660 days (10 years). Values outside that window are rejected because an absurd window would produce misleading rollups and expensive scans. 0 is explicitly allowed and means no lower-bound cutoff.","triggerScenarios":"Calling BuildStatsReport with StatsReportOptions{Days: n} where n is negative or greater than 3660, e.g. --days 9999 or a negative value computed from dates.","commonSituations":"A CLI script computing days from two dates where start > end yields a negative number; a typo like 36060 instead of 360; forwarding a raw user-supplied value without clamping.","solutions":["Clamp or validate the days value to 0..3660 before constructing StatsReportOptions","Fix the date computation that produced a negative day count (swap start/end or take absolute value)","If you truly need > 3660 days, chunk the report into multiple windows and merge"],"exampleFix":"// before\nreport, err := store.BuildStatsReport(StatsReportOptions{Days: days}) // days = 5000\n// after\nif days < 0 || days > 3660 {\n    return fmt.Errorf(\"--days must be 0..3660\")\n}\nreport, err := store.BuildStatsReport(StatsReportOptions{Days: days})","handlingStrategy":"validation","validationCode":"func normalizeDays(d int) (int, error) {\n    if d < 0 || d > 3660 { return 0, fmt.Errorf(\"days must be 0..3660, got %d\", d) }\n    return d, nil\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Clamp user-supplied windows to the supported max before calling","Compute day counts from dates carefully (handle start>end explicitly)","Document that 0 means 'no lower bound / all time'","Add range checks wherever StatsReportOptions is constructed from raw input"],"tags":["validation","stats","cli"],"backgroundTag":"value-out-of-range","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}