{"record":{"id":"0475353f5cbf861c","repo":"Hmbown/CodeWhale","slug":"unknown-category-for-e-id-signal","errorCode":null,"errorMessage":"Unknown category for ${e.id}.","messagePattern":"Unknown category for (.+?)\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/src/core/signal.ts","lineNumber":23,"sourceCode":"  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 {\n      const left = (a + 1) * binMs - e.startTime, right = e.endTime - b * binMs, tokenRate = tokens / d;\n      fine.activeMs[at + a] += left;\n      fine.activeMs[at + b] += right;\n      fine.outputTokens[at + a] += left * tokenRate;\n      fine.outputTokens[at + b] += right * tokenRate;\n      if (b > a + 1) {\n        activeDiff[diff + a + 1] += binMs; activeDiff[diff + b] -= binMs;\n        tokenDiff[diff + a + 1] += binMs * tokenRate; tokenDiff[diff + b] -= binMs * tokenRate;\n      }","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/src/core/signal.ts#L5-L41","documentation":"buildPyramid() maps each event's category to a fixed channel via CATEGORIES.indexOf(e.category). It throws this per-event error when the category string is not one of the known channel names, because the event could not be attributed to any pyramid channel.","triggerScenarios":"An event in the array has a category not present in the library's CATEGORIES list — a typo (e.g. 'Tool' vs 'tool'), a renamed channel in a newer library version, or a custom category invented by the caller.","commonSituations":"Importing traces from another tool whose category vocabulary differs (e.g. OTLP span names); a library upgrade that renamed a category while old recorded traces still use the old name; case-sensitivity mistakes ('LLM' vs 'llm').","solutions":["Map the event's category to one of the library's supported CATEGORIES before calling buildPyramid (the error names the event id to find it).","Check the exported CATEGORIES list and match exact casing/spelling.","Add a fallback in your import layer that coerces unknown categories to a default channel (e.g. 'other') instead of passing them through.","If this appeared after a library upgrade, re-export/normalize old traces to the new category names."],"exampleFix":"// before\nbuildPyramid([{ id: 'a', startTime: 0, endTime: 10, category: 'Tool' }]);\n// after\nbuildPyramid([{ id: 'a', startTime: 0, endTime: 10, category: 'tool' }]); // matches CATEGORIES exactly","handlingStrategy":"validation","validationCode":"const KNOWN = new Set(CATEGORIES); // exported by signal module\nconst normalized = events.map(e => KNOWN.has(e.category) ? e : { ...e, category: 'other' });","typeGuard":"function hasKnownCategory(e: WhaleEvent): boolean { return CATEGORIES.includes(e.category); }","tryCatchPattern":"try { buildPyramid(events, duration); } catch (e) { const m = /Unknown category for (.+)\\./.exec(e.message); if (m) { remapCategory(m[1], 'other'); buildPyramid(events, duration); } else throw e; }","preventionTips":["Map external category vocabularies (OTLP, JSONL) to CATEGORIES at ingest.","Keep a single mapping table; never hand-type category strings at call sites.","Re-normalize stored traces after library upgrades that rename channels."],"tags":["validation","events","enum"],"backgroundTag":"invalid-enum-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"}