{"record":{"id":"bad3414cd412e64d","repo":"mastra-ai/mastra","slug":"label-stage-source-must-be-an-object","errorCode":null,"errorMessage":"${label}.${stage}.${source} 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":149,"sourceCode":"\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');\n  validateBoardRules(rules.review, 'Factory rules.review');\n\n  if (!isPlainObject(rules.tools)) throw new FactoryRuleValidationError('Factory rules.tools must be an object.');","sourceCodeStart":131,"sourceCodeEnd":167,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/rules/validation.ts#L131-L167","documentation":"Each stage+source leaf in a board must be a plain object holding at most onEnter/onExit handlers. This error fires when the leaf value for a recognized source key is not a plain object.","triggerScenarios":"rules.work.<stage>.<source> (e.g. rules.work.plan['github-pr']) is a function, null, array, or primitive instead of { onEnter?, onExit? }, during assertFactoryRules.","commonSituations":"Assigning a handler function directly to the source instead of nesting it under onEnter/onExit; null placeholder values; accidental overwriting of a leaf object with a single callback during merges.","solutions":["Wrap the callback in a leaf object: { onEnter: handler } or { onExit: handler }.","Replace null placeholders with {} or omit the source entirely.","If multiple handlers were merged, ensure the merged value is still an object with onEnter/onExit keys."],"exampleFix":"// before\nwork: { plan: { 'github-pr': onPlanEnter } }\n// after\nwork: { plan: { 'github-pr': { onEnter: onPlanEnter } } }","handlingStrategy":"type-guard","validationCode":"function isRuleLeaf(v) {\n  return v !== null && typeof v === 'object' && !Array.isArray(v) &&\n    Object.keys(v).every(k => k === 'onEnter' || k === 'onExit');\n}\nfor (const [stage, sources] of Object.entries(rules.work ?? {})) {\n  for (const [source, leaf] of Object.entries(sources ?? {})) {\n    if (!isRuleLeaf(leaf)) throw new Error(`work.${stage}.${source} must be { onEnter?, onExit? }`);\n  }\n}","typeGuard":"const isRuleLeaf = (v: unknown): v is { onEnter?: FactoryRuleHandler; onExit?: FactoryRuleHandler } =>\n  typeof v === 'object' && v !== null && !Array.isArray(v);","tryCatchPattern":"try {\n  assertFactoryRules(rules);\n} catch (e) {\n  if (e.name === 'FactoryRuleValidationError') {\n    const path = e.message.match(/^(\\S+) must be an object/)?.[1];\n    console.error(`Malformed rule leaf at ${path}; expected { onEnter?, onExit? }`);\n  } else throw e;\n}","preventionTips":["Always wrap callbacks in { onEnter: fn } / { onExit: fn } leaf objects.","Type leaves with the FactoryBoardRules type; avoid unknown/any when assembling rules.","Reject null leaf placeholders in your config loader (map to {})."],"tags":["validation","structure","handlers","factory-rules"],"backgroundTag":"schema-validation-failed","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}