{"record":{"id":"918da09dc3113527","repo":"Hmbown/CodeWhale","slug":"unknown-category-a-category-use-other-plus-subtype-for","errorCode":null,"errorMessage":"Unknown category \"${a.category}\". Use \"other\" plus subtype for extensions.","messagePattern":"Unknown category \"(.+?)\"\\. Use \"other\" plus subtype for extensions\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/src/core/ingest.ts","lineNumber":131,"sourceCode":"}\nfunction ns(v: unknown, field: string): bigint {\n  if (typeof v === 'number' && !Number.isSafeInteger(v)) throw new Error(`${field} lost precision: encode OTLP nanoseconds as a decimal string.`);\n  const s = String(v ?? '');\n  if (!/^\\d{1,20}$/.test(s) || BigInt(s) > 18446744073709551615n) throw new Error(`Invalid ${field}: expected an OTLP nanosecond integer string.`);\n  return BigInt(s);\n}\nfunction otelStatus(v: unknown): Status {\n  const c = obj(v).code;\n  return c === 2 || c === 'STATUS_CODE_ERROR' ? 'error' : c === 1 || c === 'STATUS_CODE_OK' ? 'success' : 'unknown';\n}\nfunction normalizedEvent(v: unknown, index: number): WhaleEvent {\n  const a = obj(v), id = str(a.id), traceId = str(a.traceId), name = str(a.name);\n  if (!id || !traceId || !name) throw new Error(`Record ${index + 1} requires nonempty id, traceId, and name.`);\n  if (a.schemaVersion !== undefined && a.schemaVersion !== 1) throw new Error(`Record ${index + 1}: unsupported schemaVersion ${a.schemaVersion}.`);\n  const startTime = number(a.startTime, 'startTime', false)!;\n  const endTime = number(a.endTime, 'endTime') ?? startTime;\n  if (endTime < startTime) throw new Error(`Record ${index + 1}: endTime precedes startTime.`);\n  if (a.category !== undefined && !CATEGORIES.includes(a.category)) throw new Error(`Unknown category \"${a.category}\". Use \"other\" plus subtype for extensions.`);\n  const allowed: Status[] = ['pending', 'running', 'success', 'error', 'unknown'];\n  if (a.status !== undefined && !allowed.includes(a.status)) throw new Error(`Record ${index + 1}: invalid status.`);\n  const at = obj(a.attributes);\n  return {\n    schemaVersion: 1, id, traceId, name, parentId: str(a.parentId), startTime, endTime,\n    openEnded: a.endTime === undefined || a.openEnded === true,\n    agentId: str(a.agentId) ?? 'unattributed', agentType: str(a.agentType), parentAgentId: str(a.parentAgentId),\n    category: a.category ?? categoryFor(name, at), subtype: str(a.subtype),\n    model: str(a.model), provider: str(a.provider), tool: str(a.tool),\n    inputTokens: nonnegative(a.inputTokens, 'inputTokens'), outputTokens: nonnegative(a.outputTokens, 'outputTokens'),\n    cachedTokens: nonnegative(a.cachedTokens, 'cachedTokens'), cost: nonnegative(a.cost, 'cost'),\n    costCurrency: str(a.costCurrency), latency: nonnegative(a.latency, 'latency'),\n    contextTokens: nonnegative(a.contextTokens, 'contextTokens'), contextLimit: nonnegative(a.contextLimit, 'contextLimit'),\n    retry: nonnegative(a.retry, 'retry'), status: a.status ?? 'unknown',\n    sourceId: str(a.sourceId), targetId: str(a.targetId), targetType: str(a.targetType),\n    links: list(a.links).filter(x => typeof x?.traceId === 'string' && typeof x?.spanId === 'string'),\n    attributes: at, payload: a.payload, raw: a.raw ?? v,\n    observation: a.observation === undefined ? undefined : validateObservation(a.observation),","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/src/core/ingest.ts#L113-L149","documentation":"normalizedEvent only accepts category values listed in the CATEGORIES allowlist. An unknown category string is rejected so the event taxonomy stays closed; extension is done via category 'other' plus a free-form subtype, as the message says.","triggerScenarios":"Passing a record with a.category set to any string not in CATEGORIES, e.g. category:'database', category:'llm', or a misspelled value like 'catergory' typo entries. Any ingest call path that builds WhaleEvent records with custom categories.","commonSituations":"Mappings from other taxonomy systems (OTel span kinds, Datadog resource types) that don't use this library's closed set; typos; older exports predating a category rename; inventing a new category instead of using 'other'+subtype.","solutions":["Change the category to one of the accepted CATEGORIES values.","Use category 'other' and move the descriptive value into the subtype field.","Check CATEGORIES in pet/src/core/ingest.ts for the exact allowed strings and match casing exactly."],"exampleFix":"// before\n{ \"id\": \"a1\", \"traceId\": \"t1\", \"name\": \"db.query\", \"category\": \"database\" }\n// after\n{ \"id\": \"a1\", \"traceId\": \"t1\", \"name\": \"db.query\", \"category\": \"other\", \"subtype\": \"database\" }","handlingStrategy":"validation","validationCode":"const CATEGORIES = ['other' /* + the rest from pet/src/core/ingest.ts */];\nif (record.category !== undefined && !CATEGORIES.includes(record.category))\n  throw new Error(`category must be one of ${CATEGORIES.join(', ')} or 'other'+subtype`);","typeGuard":"type Category = typeof CATEGORIES[number];\nconst isCategory = (c: unknown): c is Category =>\n  typeof c === 'string' && (CATEGORIES as readonly string[]).includes(c);","tryCatchPattern":"try {\n  traces = importTrace(text, file);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Unknown category')) {\n    const bad = /category \"([^\"]+)\"/.exec(e.message)?.[1];\n    console.error(`Remap category '${bad}' to the allowed set or 'other'+subtype`);\n  } else throw e;\n}","preventionTips":["Keep a shared enum/constant for categories imported by both producer and importer.","Map external taxonomies (OTel, Datadog) to the closed set at export time.","Put descriptive values in subtype, never invent categories."],"tags":["validation","enum","ingest"],"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-23T08:17:48.524Z"}