{"record":{"id":"2d20dbcf12176b4a","repo":"Hmbown/CodeWhale","slug":"signal-duration-must-be-finite-and-nonnegative-2d20db","errorCode":null,"errorMessage":"Signal duration must be finite and nonnegative.","messagePattern":"Signal duration must be finite and nonnegative\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/src/core/signal.ts","lineNumber":14,"sourceCode":"import { CATEGORIES, clamp, errorOnsetOf, quantile, type BinLevel, type Metric, type SignalPyramid, type WhaleEvent } from './model.js';\n\nfunction emptyLevel(length: number, binMs: number): BinLevel {\n  const n = CATEGORIES.length * length;\n  return { binMs, length, onsets: new Float64Array(n), activeMs: new Float64Array(n),\n    outputTokens: new Float64Array(n), cost: new Float64Array(n), errors: new Float64Array(n), peak: new Float64Array(n) };\n}\nconst FIELDS = ['onsets', 'activeMs', 'outputTokens', 'cost', 'errors'] as const;\n/** O(events + channels × bins), including long intervals. No span-length inner loop. */\nexport function buildPyramid(events: readonly WhaleEvent[], requestedDuration?: number, maxBins = 16_384): SignalPyramid {\n  if (!Number.isInteger(maxBins) || maxBins < 16 || maxBins > 1_048_576) throw new Error('maxBins must be an integer in [16, 1048576].');\n  let duration = requestedDuration ?? 1;\n  for (const e of events) duration = Math.max(duration, e.endTime, e.startTime, e.status === 'error' ? errorOnsetOf(e) : 0);\n  if (!Number.isFinite(duration) || duration < 0) throw new Error('Signal duration must be finite and nonnegative.');\n  duration = Math.max(1, duration);\n  const binMs = 2 ** Math.ceil(Math.log2(Math.max(1, duration / (maxBins - 1))));\n  const length = Math.floor(duration / binMs) + 1, fine = emptyLevel(length, binMs);\n  const stride = length + 1;\n  const activeDiff = new Float64Array(CATEGORIES.length * stride), tokenDiff = new Float64Array(CATEGORIES.length * stride);\n  for (const e of events) {\n    if (!Number.isFinite(e.startTime) || !Number.isFinite(e.endTime) || e.startTime < 0 || e.endTime < e.startTime) throw new Error(`Invalid interval for ${e.id}.`);\n    const channel = CATEGORIES.indexOf(e.category);\n    if (channel < 0) throw new Error(`Unknown category for ${e.id}.`);\n    const a = Math.floor(e.startTime / binMs), b = Math.floor(e.endTime / binMs);\n    const at = channel * length, diff = channel * stride;\n    fine.onsets[at + a]++;\n    fine.cost[at + a] += e.cost ?? 0;\n    if (e.status === 'error') fine.errors[at + Math.floor(errorOnsetOf(e) / binMs)]++;\n    const d = e.endTime - e.startTime, tokens = e.outputTokens ?? 0;\n    if (d === 0) { fine.outputTokens[at + a] += tokens; continue; }\n    if (a === b) { fine.activeMs[at + a] += d; fine.outputTokens[at + a] += tokens; }\n    else {","sourceCodeStart":1,"sourceCodeEnd":32,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/src/core/signal.ts#L1-L32","documentation":"buildPyramid() derives the signal duration either from the requestedDuration parameter or by scanning events for the max endTime/startTime/error onset. It throws when the resulting duration is NaN, Infinity, or negative, because bin size and pyramid level lengths cannot be computed from such a value.","triggerScenarios":"Passing requestedDuration = NaN/Infinity/negative (e.g. an unparsed string coerced to NaN, or a duration computed as endTime - startTime with a null endTime); or events carrying non-finite endTime/startTime values that poison the Math.max scan.","commonSituations":"Duration computed from missing/undefined timestamps; a JSON import where duration was a string like \"1200\"; clock skew producing a negative span; passing Infinity as 'unbounded' which the API does not accept.","solutions":["Pass a finite, nonnegative requestedDuration explicitly (e.g. the trace's known total length in ms).","Validate computed durations before calling: if (!Number.isFinite(d) || d < 0) reject or fall back to a default.","Sanitize event timestamps on ingest so endTime/startTime are always finite numbers.","Note the API clamps duration to at least 1ms internally, so pass 0 for empty traces rather than undefined-derived NaN."],"exampleFix":"// before\nbuildPyramid(events, trace.end - trace.start); // NaN when trace.end is null\n// after\nconst d = trace.end != null ? trace.end - trace.start : 0;\nbuildPyramid(events, Number.isFinite(d) && d >= 0 ? d : 0);","handlingStrategy":"validation","validationCode":"const d = requested ?? 0;\nif (!Number.isFinite(d) || d < 0) throw new TypeError('duration must be finite and >= 0');\nbuildPyramid(events, d);","typeGuard":"function isNonFiniteSafeDuration(v: unknown): v is number { return typeof v === 'number' && Number.isFinite(v) && v >= 0; }","tryCatchPattern":"try { buildPyramid(events, requestedDuration); } catch (e) { if (/duration must be finite/.test(e.message)) buildPyramid(events, 0); else throw e; }","preventionTips":["Never pass undefined-derived arithmetic (null timestamps) as duration.","Coerce imported durations with Number() and check isFinite.","For empty traces pass 0, not NaN or Infinity."],"tags":["validation","time","signal-processing"],"backgroundTag":"invalid-argument-value","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}