{"record":{"id":"c84e187e94ecb406","repo":"mastra-ai/mastra","slug":"rule-metadata-must-not-contain-cycles","errorCode":null,"errorMessage":"Rule metadata must not contain cycles.","messagePattern":"Rule metadata must not contain cycles\\.","errorType":"validation","errorClass":"FactoryRuleValidationError","httpStatus":null,"severity":"error","filePath":"mastracode/factory/src/rules/validation.ts","lineNumber":105,"sourceCode":"    throw new FactoryRuleValidationError(`${label} is invalid.`);\n  }\n  return value as T;\n}\n\nexport function normalizeFactoryRuleJsonValue(\n  value: unknown,\n  depth = 0,\n  seen = new Set<object>(),\n): FactoryRuleJsonValue {\n  if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;\n  if (typeof value === 'number') {\n    if (!Number.isFinite(value)) throw new FactoryRuleValidationError('Rule metadata must contain finite numbers.');\n    return value;\n  }\n  if (depth >= MAX_JSON_DEPTH || (typeof value !== 'object' && !Array.isArray(value))) {\n    throw new FactoryRuleValidationError('Rule metadata is not bounded JSON.');\n  }\n  if (seen.has(value as object)) throw new FactoryRuleValidationError('Rule metadata must not contain cycles.');\n  seen.add(value as object);\n  try {\n    if (Array.isArray(value)) {\n      if (value.length > MAX_JSON_COLLECTION_SIZE) {\n        throw new FactoryRuleValidationError('Rule metadata contains too many entries.');\n      }\n      return value.map(entry => normalizeFactoryRuleJsonValue(entry, depth + 1, seen));\n    }\n    if (!isPlainObject(value)) throw new FactoryRuleValidationError('Rule metadata must use plain objects.');\n    const entries = Object.entries(value);\n    if (entries.length > MAX_JSON_COLLECTION_SIZE) {\n      throw new FactoryRuleValidationError('Rule metadata contains too many fields.');\n    }\n    const sanitized: Record<string, FactoryRuleJsonValue> = {};\n    for (const [key, entry] of entries) {\n      const normalizedKey = boundedString(key, 'Rule metadata key', 128, IDENTIFIER_RE);\n      sanitized[normalizedKey] = SENSITIVE_KEY_RE.test(normalizedKey)\n        ? '[REDACTED]'","sourceCodeStart":87,"sourceCodeEnd":123,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/rules/validation.ts#L87-L123","documentation":"Cyclic object graphs cannot be JSON-serialized, so normalizeFactoryRuleJsonValue tracks visited containers (with a seen set that allows shared non-cyclic references via backtracking in the finally block) and throws if the same object appears twice along one path. This prevents infinite recursion and invalid metadata.","triggerScenarios":"Metadata containing an object that references itself (obj.self = obj) or a mutual cycle (a.child = b; b.parent = a), commonly from graph-like structures, linked lists with back-pointers, or error objects with circular cause chains spread into metadata.","commonSituations":"Spreading objects with parent/back references (DOM-like trees, AST nodes with parent pointers), reusing a logger or context object that closes over its container, merging configs where a value ends up referencing an ancestor.","solutions":["Break cycles: remove back-references (e.g. delete node.parent) before building metadata.","Construct metadata as fresh plain-object literals rather than spreading existing graph structures.","Serialize with a cycle-safe replacer, or copy only primitive leaves into metadata.","Run a quick cycle check before calling the API (e.g. try JSON.stringify(value) and catch the TypeError)."],"exampleFix":"// before\nmetadata: { item, item: { parent: item } }\n// after\nconst { parent, ...safe } = item;\nmetadata: { item: safe }","handlingStrategy":"validation","validationCode":"const hasCycle = (root: unknown): boolean => {\n  const seen = new Set<object>(); const visit = (v: unknown): boolean => {\n    if (!v || typeof v !== 'object') return false;\n    if (seen.has(v)) return true;\n    seen.add(v);\n    return Object.values(v).some(visit);\n  };\n  return visit(root);\n};\nif (hasCycle(metadata)) throw new TypeError('metadata contains a cycle');","typeGuard":"const isAcyclic = (v: unknown, seen = new Set<object>()): boolean => {\n  if (!v || typeof v !== 'object') return true;\n  if (seen.has(v)) return false;\n  seen.add(v);\n  return Object.values(v).every(x => isAcyclic(x, seen));\n};","tryCatchPattern":"try {\n  JSON.stringify(metadata); // throws TypeError on cycles too\n  emit({ type: 'upsertLinkedWorkItem', metadata });\n} catch (e) {\n  if (e instanceof TypeError && /circular|cyclic/i.test(e.message)) {\n    emit({ type: 'upsertLinkedWorkItem', metadata: JSON.parse(JSON.stringify(metadata, cycleReplacer())) });\n  } else throw e;\n}","preventionTips":["Never spread objects that carry parent/back-pointers into metadata.","Build metadata as fresh object literals from primitives.","Dry-run with JSON.stringify(metadata) in tests — it throws on cycles.","Strip known back-reference fields (parent, container, owner) before attaching objects."],"tags":["validation","json","cycles"],"backgroundTag":"circular-json-reference","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}