{"record":{"id":"4c0e109478563872","repo":"koala73/worldmonitor","slug":"initramp-rampcurve-entries-must-be-positive-inte","errorCode":null,"errorMessage":"[initRamp] rampCurve entries must be positive integers","messagePattern":"\\[initRamp\\] rampCurve entries must be positive integers","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"convex/broadcast/rampRunner.ts","lineNumber":169,"sourceCode":"    seedLastWaveBroadcastId: v.optional(v.string()),\n    seedLastWaveSentAt: v.optional(v.number()),\n    seedLastWaveLabel: v.optional(v.string()),\n    seedLastWaveSegmentId: v.optional(v.string()),\n    seedLastWaveAssigned: v.optional(v.number()),\n    // Locale filter — when true, pickWaveAction excludes contacts with\n    // non-English locale (canonical via `users.localePrimary`, fallback\n    // via email-TLD heuristic for legacy waitlist registrants without a\n    // users row). Default false to preserve byte-identical behavior for\n    // existing ramps. Operators opt in for wave-8+ deliberately. Run\n    // `_dryRunNonEnglishExclusion` first to inspect impact before flipping.\n    excludeNonEnglish: v.optional(v.boolean()),\n  },\n  handler: async (ctx, args) => {\n    if (args.rampCurve.length === 0) {\n      throw new Error(\"[initRamp] rampCurve must be non-empty\");\n    }\n    if (args.rampCurve.some((n) => !Number.isInteger(n) || n <= 0)) {\n      throw new Error(\"[initRamp] rampCurve entries must be positive integers\");\n    }\n    const offset = args.waveLabelOffset ?? 0;\n    const hasSeedBroadcast = !!args.seedLastWaveBroadcastId;\n    const hasSeedSentAt = typeof args.seedLastWaveSentAt === \"number\";\n    if (hasSeedBroadcast !== hasSeedSentAt) {\n      throw new Error(\n        \"[initRamp] seedLastWaveBroadcastId and seedLastWaveSentAt must be provided together.\",\n      );\n    }\n    if (offset > 0 && !hasSeedBroadcast) {\n      throw new Error(\n        `[initRamp] waveLabelOffset=${offset} signals resumption after manual waves; seedLastWaveBroadcastId + seedLastWaveSentAt are required so the first cron tick can apply the kill-gate against the prior wave. Pass them, or set waveLabelOffset=0 to start a fresh ramp.`,\n      );\n    }\n    const existing = await ctx.db\n      .query(\"broadcastRampConfig\")\n      .withIndex(\"by_key\", (q) => q.eq(\"key\", RAMP_KEY))\n      .first();","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/broadcast/rampRunner.ts#L151-L187","documentation":"Thrown by initRamp when any entry in args.rampCurve fails Number.isInteger or is <= 0. Each curve entry is a per-wave recipient count and must be a positive integer — fractional, zero, or negative counts are meaningless for audience sizing and would break the reservoir-sampling math downstream.","triggerScenarios":"Calling initRamp with rampCurve containing a zero (e.g. [100, 0, 500]), a negative number, a fractional value like 100.5, or a value that is not a number (NaN coerced).","commonSituations":"Curve read from a JSON config that had a null/0 placeholder; a computed curve where a percentage multiplied down to a fraction; a templating bug inserted a non-numeric value.","solutions":["Ensure every entry in rampCurve is a positive integer (>= 1).","Round/ceil computed counts to integers and clamp to a minimum of 1 before passing.","Validate the curve with a pre-check: curve.every(n => Number.isInteger(n) && n > 0)."],"exampleFix":"// before\nawait initRamp(ctx, { rampCurve: [100, 0, 500.5], waveLabelPrefix: \"w\" });\n// after\nawait initRamp(ctx, { rampCurve: [100, 50, 501], waveLabelPrefix: \"w\" });","handlingStrategy":"validation","validationCode":"if (!rampCurve.every(n => Number.isInteger(n) && n > 0)) {\n  throw new Error(\"rampCurve entries must be positive integers\");\n}\nawait initRamp(ctx, { rampCurve, waveLabelPrefix });","typeGuard":"function isPositiveIntegerCurve(c: unknown): c is number[] {\n  return Array.isArray(c) && c.every(n => Number.isInteger(n) && (n as number) > 0);\n}","tryCatchPattern":null,"preventionTips":["Clamp/ceil computed curve values to positive integers.","Validate the curve in a config-lint step.","Never allow null/0 placeholders in a production ramp curve."],"tags":["convex","broadcast","ramp","validation","input-validation"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}