{"record":{"id":"706b7181c60aeeb3","repo":"CherryHQ/cherry-studio","slug":"theme-contract-mode-variable-cycle-sta","errorCode":null,"errorMessage":"[theme-contract] ${mode} variable cycle: ${[...stack.slice(cycleStart), name].join(' -> ')}","messagePattern":"\\[theme-contract\\] (.+?) variable cycle: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/ui/scripts/validate-theme-contract.ts","lineNumber":204,"sourceCode":"      if (!declarations.has(reference)) {\n        throw new Error(\n          `[theme-contract] ${mode} ${declaration.name} in ${declaration.source} references undefined ${reference}`\n        )\n      }\n    }\n  }\n}\n\nfunction assertNoCycles(mode: string, declarations: Map<string, Declaration>): void {\n  const visited = new Set<string>()\n  const visiting = new Set<string>()\n  const stack: string[] = []\n\n  const visit = (name: string): void => {\n    if (visited.has(name)) return\n    if (visiting.has(name)) {\n      const cycleStart = stack.indexOf(name)\n      throw new Error(`[theme-contract] ${mode} variable cycle: ${[...stack.slice(cycleStart), name].join(' -> ')}`)\n    }\n\n    visiting.add(name)\n    stack.push(name)\n\n    const declaration = declarations.get(name)\n    if (declaration) {\n      for (const reference of extractReferences(declaration.value, declaration.source)) {\n        if (declarations.has(reference)) visit(reference)\n      }\n    }\n\n    stack.pop()\n    visiting.delete(name)\n    visited.add(name)\n  }\n\n  for (const name of declarations.keys()) visit(name)","sourceCodeStart":186,"sourceCodeEnd":222,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/packages/ui/scripts/validate-theme-contract.ts#L186-L222","documentation":"Thrown by assertNoCycles when the var() reference graph in a mode contains a cycle (A -> B -> ... -> A). The validator does a DFS over references; revisiting a node currently on the visit stack is a cycle. The message prints the cycle path so you can see exactly which variables point at each other.","triggerScenarios":"`--inline-code: var(--inline-code-foreground);` together with `--inline-code-foreground: var(--inline-code);` in product.css; rewiring two variables to point at each other during a dark-mode refactor; a chain like `--a: var(--b); --b: var(--c); --c: var(--a);`.","commonSituations":"Swapping which variable is the 'source of truth' and forgetting to break the old back-reference; merge conflicts that combine two half-refactors.","solutions":["Inspect the cycle path in the message and re-point at least one declaration in the cycle at a concrete value or a variable OUTSIDE the cycle.","Pick a single owner for the underlying value and have the other variable(s) reference it one-directionally.","Re-run `pnpm --filter @cherrystudio/ui theme:check` and the reference/cycle assertions together."],"exampleFix":"// before (product.css)\n--inline-code: var(--inline-code-foreground);\n--inline-code-foreground: var(--inline-code);\n// after — one-directional\n--inline-code: rgba(0, 0, 0, 0.06);\n--inline-code-foreground: rgb(218, 97, 92);","handlingStrategy":"validation","validationCode":"// Detect cycles via DFS over the var() reference graph before calling the validator.\nfunction hasCycle(decls: Map<string, string>) {\n  const color = new Map<string, 0 | 1 | 2>()\n  const dfs = (n: string): boolean => {\n    color.set(n, 1)\n    for (const [, ref] of (decls.get(n) ?? '').matchAll(/var\\(\\s*(--[a-z0-9-]+)/g)) {\n      if (!decls.has(ref)) continue\n      if (color.get(ref) === 1) return true\n      if (color.get(ref) === undefined && dfs(ref)) return true\n    }\n    color.set(n, 2)\n    return false\n  }\n  return [...decls.keys()].some((n) => color.get(n) === undefined && dfs(n))\n}","typeGuard":null,"tryCatchPattern":"try {\n  validateThemeContractSources(sources)\n} catch (error) {\n  if (error instanceof Error && /variable cycle/.test(error.message)) {\n    console.error(error.message) // prints the cycle path\n    process.exitCode = 1\n    return\n  }\n  throw error\n}","preventionTips":["When swapping which variable is the source of truth, ensure the old back-reference is removed.","Prefer one-directional chains: one concrete owner, others reference it.","Run theme:check after rewiring var() references."],"tags":["theme-contract","css","build","validation"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}