{"record":{"id":"81d1b05aa4021396","repo":"mastra-ai/mastra","slug":"label-stage-must-be-an-object","errorCode":null,"errorMessage":"${label}.${stage} must be an object.","messagePattern":"(.+?)\\.(.+?) must be an object\\.","errorType":"validation","errorClass":"FactoryRuleValidationError","httpStatus":null,"severity":"error","filePath":"mastracode/factory/src/rules/validation.ts","lineNumber":146,"sourceCode":"    seen.delete(value as object);\n  }\n}\n\nfunction sanitizeMetadata(value: unknown): Record<string, FactoryRuleJsonValue> | undefined {\n  if (value === undefined) return undefined;\n  const sanitized = normalizeFactoryRuleJsonValue(value);\n  if (!isPlainObject(sanitized)) throw new FactoryRuleValidationError('Rule metadata must be an object.');\n  if (JSON.stringify(sanitized).length > MAX_METADATA_JSON_LENGTH) {\n    throw new FactoryRuleValidationError('Rule metadata is too large.');\n  }\n  return sanitized;\n}\n\nfunction validateBoardRules(rules: unknown, label: string): asserts rules is FactoryBoardRules {\n  if (!isPlainObject(rules)) throw new FactoryRuleValidationError(`${label} must be an object.`);\n  for (const [stage, sources] of Object.entries(rules)) {\n    enumValue(stage, FACTORY_RULE_STAGES, `${label} stage`);\n    if (!isPlainObject(sources)) throw new FactoryRuleValidationError(`${label}.${stage} must be an object.`);\n    for (const [source, leaf] of Object.entries(sources)) {\n      enumValue(source, FACTORY_RULE_SOURCES, `${label}.${stage} source`);\n      if (!isPlainObject(leaf)) throw new FactoryRuleValidationError(`${label}.${stage}.${source} must be an object.`);\n      assertExactKeys(leaf, ['onEnter', 'onExit'], `${label}.${stage}.${source}`);\n      for (const handler of Object.values(leaf)) {\n        if (handler !== undefined && typeof handler !== 'function') {\n          throw new FactoryRuleValidationError(`${label}.${stage}.${source} handlers must be functions.`);\n        }\n      }\n    }\n  }\n}\n\nexport function assertFactoryRules(rules: unknown): asserts rules is FactoryRules {\n  if (!isPlainObject(rules)) throw new FactoryRuleValidationError('Factory rules must be an object.');\n  assertExactKeys(rules, ['version', 'work', 'review', 'tools', 'github', 'linear'], 'Factory rules');\n  boundedString(rules.version, 'Factory rule version', MAX_VERSION_LENGTH);\n  validateBoardRules(rules.work, 'Factory rules.work');","sourceCodeStart":128,"sourceCodeEnd":164,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/rules/validation.ts#L128-L164","documentation":"Within a board, each key must be a valid FACTORY_RULE_STAGES enum value whose value is a plain object mapping sources to leaf handlers. This error fires when a stage entry's value is not a plain object (after the stage name itself passed the enum check).","triggerScenarios":"rules.work.<stage> or rules.review.<stage> set to an array, null, string, or class instance while <stage> is a valid stage name, evaluated during assertFactoryRules via prepare or defaultFactoryRules.","commonSituations":"Defining stages as arrays of handlers (rules.work.plan = [fn]); typo'd nested structure where a handler was hoisted to the stage level; config that collapsed a level after a schema change.","solutions":["Make the stage value an object keyed by valid source names: { 'github-issue': { onEnter: fn, onExit: fn } }.","Convert arrays of handlers into per-source leaf objects.","Verify no extra nesting level was removed/added when refactoring the rules shape."],"exampleFix":"// before\nwork: { plan: [myEnterHandler] }\n// after\nwork: { plan: { 'github-issue': { onEnter: myEnterHandler } } }","handlingStrategy":"validation","validationCode":"const STAGES = ['todo', 'in-progress', 'review', 'done']; // match FACTORY_RULE_STAGES\nfor (const [stage, sources] of Object.entries(rules.work ?? {})) {\n  if (!STAGES.includes(stage)) throw new Error(`Unknown stage: ${stage}`);\n  if (typeof sources !== 'object' || sources === null || Array.isArray(sources)) {\n    throw new Error(`rules.work.${stage} must be a plain object of source -> handlers`);\n  }\n}","typeGuard":"const isStageMap = (v: unknown): v is Record<string, Record<string, unknown>> =>\n  typeof v === 'object' && v !== null && !Array.isArray(v) &&\n  Object.values(v).every(s => typeof s === 'object' && s !== null && !Array.isArray(s));","tryCatchPattern":"try {\n  prepare(rules);\n} catch (e) {\n  if (e.name === 'FactoryRuleValidationError' && /must be an object/.test(e.message)) {\n    console.error(`Fix rules structure at: ${e.message.split(' ')[0]}`);\n  } else throw e;\n}","preventionTips":["Model rules with the FactoryBoardRules type so stage/source nesting is enforced statically.","Keep exactly three levels per board: stage -> source -> { onEnter, onExit }.","Write a snapshot test over your default rules object that runs assertFactoryRules."],"tags":["validation","structure","factory-rules","type-mismatch"],"backgroundTag":"schema-validation-failed","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}