{"record":{"id":"2ecca31699a56652","repo":"actualbudget/actual","slug":"unknown-cleanup-row-type-string-row","errorCode":null,"errorMessage":"Unknown cleanup row type: ${String(row)}","messagePattern":"Unknown cleanup row type: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/budget/cleanup-template-notes.ts","lineNumber":110,"sourceCode":"    case 'source':\n      return { role: 'source', groupId: resolveGroup(row.group, nameToId) };\n    case 'sink':\n      return {\n        role: 'sink',\n        groupId: resolveGroup(row.group, nameToId),\n        weight: row.weight,\n      };\n    case 'overspend': {\n      const groupId = nameToId.get(row.group.toLowerCase());\n      if (groupId == null) {\n        throw new Error(\n          `Unresolved cleanup group for overspend row: ${row.group}`,\n        );\n      }\n      return { role: 'overspend', groupId };\n    }\n    default:\n      throw new Error(`Unknown cleanup row type: ${String(row)}`);\n  }\n}\n\nfunction resolveGroup(\n  name: string | null,\n  nameToId: Map<string, string>,\n): string | null {\n  return name != null ? (nameToId.get(name.toLowerCase()) ?? null) : null;\n}\n\nasync function resolveCleanupGroups(\n  names: ReadonlySet<string>,\n): Promise<Map<string, string>> {\n  const map = new Map<string, string>();\n  for (const name of names) {\n    const id = await resolveCleanupGroup(name);\n    map.set(name.toLowerCase(), id);\n  }","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/budget/cleanup-template-notes.ts#L92-L128","documentation":"toCleanupTemplate dispatches on a discriminated union of cleanup row types; the default branch throws when a row has an unrecognized `type`. This means the row data doesn't conform to the known cleanup row schema (group, overspend, etc.) and cannot be converted into a cleanup template. It is a defensive exhaustiveness check against malformed or newer-unknown row payloads.","triggerScenarios":"Passing a row whose `type`/discriminator is misspelled, undefined, or produced by a newer version of the note parser than the converter understands; e.g. `#cleanup prioritize ...` parsed into a row type not handled by this switch.","commonSituations":"Version mismatch between the code that parses cleanup notes and the code that converts them, hand-built row objects with typos in the type field, or corrupted/stale persisted template data.","solutions":["Inspect the offending row's type value and correct it to a supported cleanup row type.","Update Actual to a version whose toCleanupTemplate handles the row type being used.","Fix the parser/builder that produced the row so it only emits known types.","If persisting templates, clear/repair the stale stored rows."],"exampleFix":"// before\n{ type: 'overspendd', group: 'Groceries' }\n// after\n{ type: 'overspend', group: 'Groceries' }","handlingStrategy":"type-guard","validationCode":"const KNOWN_TYPES = new Set(['group', 'overspend']);\nconst unknown = rows.filter(r => !KNOWN_TYPES.has(r.type));\nif (unknown.length > 0) {\n  throw new Error(`Unknown cleanup row types: ${unknown.map(r => r.type).join(', ')}`);\n}","typeGuard":"type GroupRow = { type: 'group'; name: string; weight?: number };\ntype OverspendRow = { type: 'overspend'; group: string; weight?: number };\nfunction isCleanupRow(r: unknown): r is GroupRow | OverspendRow {\n  return (\n    typeof r === 'object' && r !== null &&\n    'type' in r &&\n    ((r as GroupRow).type === 'group' || (r as OverspendRow).type === 'overspend')\n  );\n}","tryCatchPattern":"try {\n  const cleanup = toCleanupTemplate(rows);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Unknown cleanup row type')) {\n    logger.warn('Skipping cleanup template with unknown row type', { error: e.message });\n  } else {\n    throw e;\n  }\n}","preventionTips":["Validate row objects against the cleanup row union type before conversion","Keep note parsers and template converters on the same version","Use a discriminated union with exhaustive switch (never silenty default) in your own code","Sanitize/repair persisted template data after upgrades"],"tags":["budget-cleanup","validation","schema-mismatch"],"backgroundTag":"unknown-enum-value","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}